Greasy Fork is available in English.

Skrypt umożliwiający pobieranie materiałów ze znanych serwisów VOD.

Skrypt służący do pobierania materiałów ze znanych serwisów VOD.

// ==UserScript==
// @name           Skrypt umożliwiający pobieranie materiałów ze znanych serwisów VOD.
// @version        7.2.4
// @description    Skrypt służący do pobierania materiałów ze znanych serwisów VOD.
//                 Działa poprawnie tylko z rozszerzeniem Tampermonkey.
//                 Cześć kodu pochodzi z:
//                 miniskrypt.blogspot.com,
//                 miniskrypt.hubaiitv.pl
// @author         Przmus, zacny
// @namespace      http://www.ipla.tv/
// @source         https://github.com/zacny/voddownloader
// @include        /^https://(vod|cyfrowa)\.tvp\.pl/video/.*$/
// @include        /^https?://.*\.tvp.(pl|info)/sess/TVPlayer2/embed.*$/
// @include        /^https?://((?!wiadomosci).)*\.tvp\.pl/\d{6,}/.*$/
// @include        https://www.tvpparlament.pl/sess/*
// @include        https://www.ipla.tv/*
// @include        https://player.pl/*
// @include        https://*.cda.pl/*
// @include        https://vod.pl/*
// @include        https://redir.atmcdn.pl/*
// @include        https://*.redcdn.pl/file/o2/redefine/partner/*
// @include        https://partner.ipla.tv/embed/*
// @include        https://wideo.wp.pl/*
// @include        https://ninateka.pl/*
// @include        https://www.arte.tv/*/videos/*
// @include        https://pulsembed.eu/*
// @include        https://tv-trwam.pl/local-vods/*
// @exclude        http://www.tvp.pl/sess/*
// @exclude        /^https?://(bialystok|gorzow|krakow|olsztyn|rzeszow|wroclaw|bydgoszcz|katowice|lublin|opole|szczecin|gdansk|kielce|lodz|poznan|warszawa)\.tvp.\pl/.*$/
// @exclude        /^https?://.*\.vod\.tvp\.pl/\d{6,}/.*$/
// @exclude        https://www.cda.pl/iframe/*
// @grant          GM_getResourceText
// @grant          GM_xmlhttpRequest
// @grant          GM_download
// @grant          GM_setClipboard
// @grant          GM_info
// @connect        tvp.pl
// @connect        getmedia.redefine.pl
// @connect        distro.redefine.pl
// @connect        player-api.dreamlab.pl
// @connect        api.arte.tv
// @connect        b2c.redefine.pl
// @connect        player.pl
// @connect        api-trwam.app.insysgo.pl
// @run-at         document-end
// @require        https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require        https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js
// @require        https://cdnjs.cloudflare.com/ajax/libs/platform/1.3.5/platform.min.js
// @require        https://gitcdn.xyz/cdn/zacny/voddownloader/4b17a120f521eaddf476d6e8fe3be152d506f244/lib/js/mdb-with-waves-patch.js
// @require        https://gitcdn.xyz/cdn/kapetan/jquery-observe/ca67b735bb3ae8d678d1843384ebbe7c02466c61/jquery-observe.js
// @resource       buttons_css https://raw.githubusercontent.com/zacny/voddownloader/master/lib/css/voddownloader-buttons.css
// @resource       content_css https://raw.githubusercontent.com/zacny/voddownloader/master/lib/css/voddownloader-content.css
// ==/UserScript==

(function vodDownloader($, platform, Waves) {
    'use strict';

    var Exception = (function(error, templateParams) {
	    this.error = error;
	    this.templateParams = Array.isArray(templateParams) ? templateParams : [templateParams];
	});
	
	var Tool = (function(Tool) {
	    Tool.deleteParametersFromUrl = function(url){
	        return decodeURIComponent(url.replace(/\?.*/,''));
	    };
	
	    Tool.getUrlParameter = function(paramName, url){
	        var results = new RegExp('[\?&]' + paramName + '=([^&#]*)').exec(url);
	        if (results==null) {
	            return null;
	        }
	        return decodeURIComponent(results[1]) || 0;
	    };
	
	    Tool.formatConsoleMessage = function(message, params){
	        console.log.apply(this, $.merge([message], params));
	    };
	
	    var removeNotAllowedFileNameChars = function(input){
	        var regexp = new RegExp('[\/:*?"<>|]+', 'g');
	        return input.replace(regexp, '').replace(/\s+/g, ' ');
	    };
	
	    Tool.downloadFile = function(fileUrl, title){
	        var extension = Tool.deleteParametersFromUrl(fileUrl.split('.').pop());
	        var movieTitle = (title !== undefined && title !== '' ) ? title : 'nieznany';
	        var name = removeNotAllowedFileNameChars(movieTitle) + '.' + extension;
	        GM_download(fileUrl, name);
	    };
	
	    Tool.template = function(templates, ...keys){
	        return (function(...values) {
	            var dict = values[values.length - 1] || {};
	            var result = [templates[0]];
	            keys.forEach(function(key, i) {
	                var value = Number.isInteger(key) ? values[key] : dict[key];
	                result.push(value, templates[i + 1]);
	            });
	            return result.join('');
	        });
	    };
	
	    Tool.getRealUrl = function(){
	        var topUrl = window.sessionStorage.getItem(config.storage.topWindowLocation);
	        return topUrl !== null ? topUrl : window.location.href;
	    };
	
	    Tool.isTopWindow = function(){
	        return window.top === window.self;
	    };
	
	    Tool.pad = function(number, characters){
	        return(1e15+number+"").slice(-characters)
	    };
	
	    Tool.mapDescription = function(data){
	        var defaults = $.extend({}, config.description.defaults);
	        var sourceDescriptions = config.description.sources[data.source] || {};
	        var descriptionVariant = sourceDescriptions[data.key] || {};
	        return $.extend(true, defaults, data, descriptionVariant);
	    };
	
	    return Tool;
	}(Tool || {}));
	
	const config = {
	    attempts: 10,
	    attemptTimeout: 1500,
	    storage: {
	        doNotWarn: 'voddownloader.doNotwarnIfIncorrectPluginSettingsDetected',
	        topWindowLocation: 'voddownloader.topWindowLocation'
	    },
	    urlParamPattern: '#',
	    urlParamDefaultKey: 'videoId',
	    urlPartPattern: '~',
	    include: {
	        fontawesome: {
	            id: 'fontawesome',
	            css: 'https://use.fontawesome.com/releases/v5.8.2/css/all.css'
	        },
	        bootstrap: {
	            id: 'bootstrap',
	            css: 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css'
	        },
	        mdb: {
	            id: 'mdb',
	            css: 'https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.8.2/css/mdb.min.css',
	        }
	    },
	    error: {
	        id: {
	            caption: 'Nie udało się odnaleźć idetyfikatora.',
	            template: Tool.template`Algorytm rozpoznawania identyfikatora wideo na stronie: ${0} \
	                zakończył się niepowodzeniem. Może to oznaczać błąd skryptu lub zmiany w portalu.`,
	        },
	        tvnId: {
	            caption: 'Nie udało się odnaleźć idetyfikatora.',
	            template: Tool.template`Algorytm rozpoznawania identyfikatora wideo na stronie: ${0} \
	                zakończył się niepowodzeniem.\nJeżeli jest to główna strona programu oznacza to, \
	                że nie udało się odnaleźć identyfikatora ostatniego odcinka. Wejdź na stronę odcinka \
	                i spróbuj ponownie.\nMoże to również oznaczać błąd skryptu lub zmiany w portalu.`,
	        },
	        call: {
	            caption: 'Błąd pobierania informacji o materiale.',
	            template: Tool.template`Wystąpił błąd w wykonaniu skryptu w kroku: ${0} na stronie: ${1} \
	                Zgłoś problem autorom skryptu.`,
	        },
	        noSource: {
	            caption: 'Nie udało się odnaleźć metadanych tego materiału.',
	            template: Tool.template`Materiał ze strony ${0} nie posiada zdefiniowanych metadanych potrzebnych do \
	                działania skryptu lub są one nieprawidłowe.\n
	                Może to oznaczać, że nie jest to materiał publicznie dostępny, nie posiada zdefiniowanych źródeł lub nie \
	                mogą one zostać wyświetlone w przeglądarce bez dodatkowego oprogramowania albo jest to materiał \
	                umieszczony w płatnej strefie.`,
	            type: 'info'
	        },
	        timeout: {
	            caption: 'Zbyt długi czas odpowiedzi.',
	            template: Tool.template`Dla kroku: ${0} na stronie "${1}" nie dotarły \
	                informacje zwrotne.\nPrzypuszczalnie jest to problem sieciowy. Spróbuj ponownie za jakiś czas.`
	        },
	        noParent: {
	            caption: 'Brak zakładki ze stroną główną.',
	            template: Tool.template`Została zamknięta zakładka ze stroną na której został uruchomiony skrypt. \
	                    Ta zakładka nie może przez to działać poprawnie. Otwórz ponownie stronę główną: \n ${0} \n
	                    by przywrócić prawidłowe funkcjonowanie skryptu.`
	        }
	    },
	    description: {
	        defaults: {
	            index: 99,
	            language: 'polski',
	            audio:  'MPEG AAC'
	        },
	        sources: {
	            IPLA: {
	                '1080p': {video: 'H264 MPEG-4 AVC, 4011 kb/s, 1920x1080, 25fps, 16:9', index: 1},
	                '720p': {video: 'H264 MPEG-4 AVC, 1672 kb/s, 1280x720, 25fps, 16:9', index: 2},
	                '576p': {video: 'H264 MPEG-4 AVC, 1175 kb/s, 1024x576, 25fps, 16:9', index: 3},
	                '384p': {video: 'H264 MPEG-4 AVC, 256 kb/s, 484x272, 25fps, 16:9', index: 4}
	            },
	            WP: {
	                HQ: {video: 'H264 MPEG-4 AVC, 1804 kb/s, 1280x720, 24fps, 16:9', index: 1},
	                LQ: {video: 'H264 MPEG-4 AVC, 616 kb/s, 640x360, 24fps, 16:9', index: 2}
	            },
	            TVN: {
	                'HD': {video: 'H264 MPEG-4 AVC, 2776 kb/s, 1280x720, 25fps, 16:9', index: 1},
	                'Bardzo wysoka': {video: 'H264 MPEG-4 AVC, 1786 kb/s, 1280x720, 25fps, 16:9', index: 2},
	                'Wysoka': {video: 'H264 MPEG-4 AVC, 1191 kb/s, 720x576, 25fps, 5:4', index: 3},
	                'Standard': {video: 'H264 MPEG-4 AVC, 794 kb/s, 720x576, 25fps, 5:4', index: 4},
	                'Średnia': {video: 'H264 MPEG-4 AVC, 596 kb/s, 640x480, 25fps, 4:3', index: 5},
	                'Niska': {video: 'H264 MPEG-4 AVC, 417 kb/s, 512x384, 25fps, 4:3', index: 6},
	                'Bardzo niska': {video: 'H264 MPEG-4 AVC, 238 kb/s, 320x240, 25fps, 4:3', index: 7}
	            },
	            VOD: {
	                '1080':{video: 'H264 MPEG-4 AVC, 1920x1080, 25fps, 16:9', index: 1},
	                '720': {video: 'H264 MPEG-4 AVC, 1280x720, 25fps, 16:9', index: 2},
	                '576': {video: 'H264 MPEG-4 AVC, 1024x576, 25fps, 16:9', index: 3},
	                '480': {video: 'H264 MPEG-4 AVC, 854x480, 25fps, 16:9', index: 4},
	                '360': {video: 'H264 MPEG-4 AVC, 640x360, 25fps, 16:9', index: 5},
	                '240': {video: 'H264 MPEG-4 AVC, 426x240, 25fps, 16:9', index: 6}
	            },
	            TVP: {
	                '9100000': {video: 'H264 MPEG-4 AVC, 21030 kb/s, 1920x1080, 25fps, 16:9', index: 1},
	                '5420000': {video: 'H264 MPEG-4 AVC, 9875 kb/s, 1280x720, 25fps, 16:9', index: 2},
	                '2850000': {video: 'H264 MPEG-4 AVC, 4661 kb/s, 960x540, 25fps, 16:9', index: 3},
	                '1750000': {video: 'H264 MPEG-4 AVC, 1782 kb/s, 800x450, 25fps, 16:9', index: 4},
	                '1500000': {video: 'H264 MPEG-4 AVC, 1487 kb/s, 720x404, 25fps, 16:9', index: 5},
	                '1250000': {video: 'H264 MPEG-4 AVC, 1255 kb/s, 640x360, 25fps, 16:9', index: 6},
	                '820000': {video: 'H264 MPEG-4 AVC, 809 kb/s, 480x270, 25fps, 16:9', index: 7},
	                '590000': {video: 'H264 MPEG-4 AVC, 581 kb/s, 398x224, 25fps, 199:112', index: 8}
	            },
	            ARTE: {
	                '2200': {video: 'H264 MPEG-4 AVC,  2438 kb/s, 1280x720, 25fps, 16:9', index: 1},
	                '1500': {video: 'H264 MPEG-4 AVC,  1619 kb/s, 720x406, 25fps, 360:203', index: 2},
	                '800': {video: 'H264 MPEG-4 AVC,  805 kb/s, 640x360, 25fps, 16:9', index: 3},
	                '300': {video: 'H264 MPEG-4 AVC,  357 kb/s, 384x216, 25fps, 16:9', index: 4}
	            },
	            NINATEKA: {
	                'video/mp4': {video: 'H264 MPEG-4 AVC,  900 kb/s, 640x360, 25fps, 16:9', index: 1},
	                'application/x-mpegURL': {description: "H264 MPEG-4 video stream with multiple resolutions", format: "HLS", index: 2},
	                'application/dash+xml': {description: "MPEG-DASH video stream with multiple resolutions", format: "MPD", index: 3},
	            },
	            CDA: {
	                '1080p': {video: 'H264 MPEG-4 AVC, 1920x1080, 16:9', index: 1},
	                '720p': {video: 'H264 MPEG-4 AVC, 1280x720, 16:9', index: 2},
	                '480p': {video: 'H264 MPEG-4 AVC, 854x480, 427:240', index: 3},
	                '360p': {video: 'H264 MPEG-4 AVC, 640x360, 16:9', index: 4},
	            },
	            TRWAM: {
	                '3': {video: 'H264 MPEG-4 AVC, 640x360, 16:9', index: 1},
	                '2': {description: "H264 MPEG-4 video stream with multiple resolutions", format: "HLS", index: 2},
	                '9': {description: "MPEG-DASH video stream with multiple resolutions", format: "MPD", index: 3},
	            }
	        }
	    }
	};
	
	
	var Step = (function(properties){
	    var step = {
	        urlTemplateParts: [],
	        urlTemplate: '',
	        before: function(input){return input},
	        after: function (output) {return output},
	        resultUrlParams: function (input, template) {
	            var urlParams = {};
	            $.each(input, function (key, value) {
	                template = template.replace(new RegExp(config.urlParamPattern + key,'g'), value);
	                urlParams[key] = value;
	            });
	
	            return {
	                url: template,
	                urlParams: urlParams
	            };
	        },
	        resolveUrl: function (input, partIndex) {
	            return this.resultUrlParams(input, this.resolveUrlParts(partIndex));
	        },
	        isRemote: function(){
	            return this.urlTemplate.length > 0;
	        },
	        method: 'GET',
	        headers: {},
	        responseType: 'json',
	        retryErrorCodes: [],
	        methodParam: function(){return {}},
	        resolveUrlParts: function(partIndex){
	            if(this.urlTemplateParts.length){
	                return this.urlTemplate.replace(config.urlPartPattern, this.urlTemplateParts[partIndex]);
	            }
	
	            return this.urlTemplate;
	        }
	    };
	
	    return $.extend(true, step, properties);
	});
	
	var Notification = (function(Notification) {
	    var create = function(title, bodyContent, special) {
	        var specialContentClasses = special ? ' special-color white-text' : '';
	        var content = $('<div>').addClass('toast notification' + specialContentClasses).attr('role', 'alert')
	            .attr('aria-live', 'assertive').attr('aria-atomic', 'true')
	            .attr('name', special ? 'special' : 'normal').attr('data-delay', '5000');
	        var header = $('<div>').addClass('toast-header special-color-dark white-text');
	        var warnIcon = $('<i>').addClass('fas fa-exclamation-triangle pr-2');
	        var notificationTitle = $('<strong>').addClass('mr-auto').text(title);
	        var time = $('<small>').text(new Date().toLocaleTimeString());
	        var close = $('<button>').attr('type', 'button').addClass('ml-2 mb-1 close white-text')
	            .attr('data-dismiss', 'toast').attr('aria-label', 'Close')
	            .append($('<span>').attr('aria-hidden', 'true').text('\u00D7'));
	
	        if(special){
	            header.append(warnIcon);
	            content.attr('data-autohide', 'false');
	        }
	        header.append(notificationTitle).append(time).append(close);
	        var body = $('<div>').addClass('toast-body notification-body').append(bodyContent);
	
	        content.append(header).append(body);
	        return content;
	    };
	
	    Notification.show = function(options, w){
	        options = options || {};
	        var special = false;
	        if (options.hasOwnProperty('special')) {
	            special = options.special;
	        }
	        if(!options.hasOwnProperty('title') || !options.hasOwnProperty('content')){
	            return;
	        }
	
	        var rootElement = $(w.document.body);
	        var notification = create(options.title, options.content, special);
	        $('#notification-container', rootElement).append(notification);
	        $('.toast', rootElement).toast('show');
	        $('.toast', rootElement).on('hidden.bs.toast', function (){
	            $.each($(this), function(index, value) {
	                var element = $(value);
	                element.remove();
	            });
	        })
	    };
	
	    return Notification;
	}(Notification || {}));
	
	var PluginSettingsDetector = (function(PluginSettingsDetector){
	    var prepareWarningNotification = function(w) {
	        var bodyContent = $('<div>')
	            .append('Twój dodatek ma nieprawidłowe ustawienia, przez co nie możesz korzystać z opcji ')
	            .append('bezpośredniego pobierania plików. Możesz skorygować je w następujący sposób:');
	        var list = $('<ol>').addClass('m-0')
	            .append($('<li>').text('Otwórz Panel sterowania Tampermonkey i kliknij ustawienia.'))
	            .append($('<li>').text('Ogólne > Tryb konfiguracji > Expert'))
	            .append($('<li>').text('Pobieranie BETA > Tryb pobierania > API przeglądarki'))
	            .append($('<li>').text('Zapisz ustawienia, a jeżeli przeglądarka zapyta o możliwość zarządzania' +
	                ' pobieranymi plikami, należy się zgodzić'));
	        bodyContent.append(list).append(createButton(w));
	        var options = {title: 'Wykryto problem', content: bodyContent, special: true};
	        Notification.show(options, w);
	    };
	
	    var createButton = function(w){
	        return $('<button>').attr('type', 'button').addClass('btn btn-dark btn-sm m-1 pl-3 pr-3')
	            .append($('<i>').addClass('fas pr-1 fa-window-close')).append('Nie pokazuj więcej').click(function(){
	                var rootElement = $(w.document.body);
	                w.localStorage.setItem(config.storage.doNotWarn, true);
	                $('.toast.special-color', rootElement).toast('hide');
	                setTimeout(function(){
	                    $('.toast.special-color', rootElement).remove();
	                }, 1000);
	            });
	    };
	
	    var disableDownload = function(w){
	        var rootElement = $(w.document.body);
	        $('.fa-save', rootElement).closest('button').attr('disabled', true);
	    };
	
	    PluginSettingsDetector.detect = function(w){
	        var downloadMode = GM_info.downloadMode;
	        if(downloadMode !== 'browser'){
	            disableDownload(w);
	            var value = w.localStorage.getItem(config.storage.doNotWarn);
	            if(value !== 'true'){
	                prepareWarningNotification(w);
	            }
	        }
	    };
	    return PluginSettingsDetector;
	}(PluginSettingsDetector || {}));
	
	var DomTamper = (function(DomTamper){
	
	    DomTamper.injectStyle = function(w, name){
	        var head = $(w.document.head);
	        if(!head.find('style[name="' + name + '"]').length){
	            var styleElement = $('<style>').attr('type', 'text/css')
	                .attr('name', name).text((GM_getResourceText(name)));
	            head.append(styleElement);
	        }
	    };
	
	    DomTamper.injectStylesheet = function (w, setting) {
	        var head = $(w.document.head);
	        if(!head.find('link[name="' + setting.id + '"]').length){
	            var stylesheet = $('<link>').attr('name', setting.id).attr('type', 'text/css').attr('rel', 'stylesheet')
	                .attr('href', setting.css);
	            head.append(stylesheet);
	        }
	    };
	
	    var prepareHead = function(w){
	        DomTamper.injectStylesheet(w, config.include.fontawesome);
	        DomTamper.injectStylesheet(w, config.include.bootstrap);
	        DomTamper.injectStylesheet(w, config.include.mdb);
	        DomTamper.injectStyle(w, 'content_css');
	    };
	
	    var createLinks = function(w, additionalClass){
	        var links = [
	            {
	                url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RWX4EUR77CMKU',
	                icon: 'fa-hand-holding-usd',
	                tooltip: 'dotacje'
	            },
	            {
	                url: 'https://greasyfork.org/pl/scripts/6049-skrypt-umo%C5%BCliwiaj%C4%85cy-pobieranie-' +
	                    'materia%C5%82%C3%B3w-ze-znanych-serwis%C3%B3w-vod/feedback',
	                icon: 'fa-comments',
	                tooltip: 'problemy, komentarze'
	            },
	            {
	                url: 'https://github.com/zacny/voddownloader/issues',
	                icon: 'fa-bug',
	                tooltip: 'zgłoś błąd'
	            }
	        ];
	        var container = $('<div>').addClass('links-position');
	        links.forEach(function(link){
	            var button = $('<button>').attr('type', 'button').attr('title', link.tooltip)
	                .addClass('btn btn-sm m-1 p-2').addClass(additionalClass)
	                .append($('<i>').addClass('fas').addClass(link.icon).addClass('fa-2x'));
	            button.click(function(){
	                w.open(link.url);
	            });
	            container.append(button);
	        });
	        return container;
	    };
	
	    var prepareBody = function(w, pageContent, detection) {
	        appendOrReplace(w, pageContent);
	        attachWaveEffect(w, pageContent);
	        if(detection) {
	            PluginSettingsDetector.detect(w);
	        }
	    };
	
	    var appendOrReplace = function (w, pageContent) {
	        var body = $(w.document.body);
	        if(body.children().length > 0){
	            body.children(":first").replaceWith(pageContent);
	        }
	        else {
	            body.append(pageContent);
	        }
	    };
	
	    var attachWaveEffect = function(w, pageContent){
	        var buttons = pageContent.find('.btn:not(.btn-flat), .btn-floating');
	        Waves.attach(buttons, ['waves-light']);
	        Waves.init({}, w);
	    };
	
	    DomTamper.handleError = function(exception, w){
	        if(w === undefined){
	            w = window.open();
	        }
	
	        prepareHead(w);
	        var errorData = getErrorData(exception);
	        var pageContent = $('<div>').addClass('page-content');
	        pageContent.append(createErrorContent(errorData));
	        pageContent.append(createLinks(w, errorData.type === 'error' ?
	            'btn-danger' : 'special-color white-text'));
	        prepareBody(w, pageContent);
	    };
	
	    var getErrorData = function(exception){
	        var type = 'error';
	        var caption = 'Niespodziewany błąd';
	        var message = 'Natrafiono na niespodziewany błąd: ' + exception;
	        if(exception.error){
	            message = exception.error.template.apply(this, exception.templateParams).replace(/\n/g, '<br/>');
	            caption = exception.error.caption;
	            type = exception.error.type !== undefined ? exception.error.type : 'error';
	        }
	
	        return {
	            message: linkify(message),
	            caption: caption,
	            type: type
	        }
	    };
	
	    var linkify = function(text) {
	        var linkDetectionRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
	        return text.replace(linkDetectionRegex, function(url) {
	            return '<u><a class="text-white" href="' + url + '">' + url + '</a></u>';
	        });
	    };
	
	    var createErrorContent = function(errorData){
	        var typeClass = errorData.type === 'error' ? 'bg-danger' : 'bg-dark';
	        var card = $('<div>').addClass('card text-white mb-3').addClass(typeClass);
	        var cardHeader = $('<div>').addClass('card-header')
	            .text('Niestety natrafiono na problem, który uniemożliwił dalsze działanie');
	        var cardBody = $('<div>').addClass('card-body')
	            .append($('<h5>').addClass('card-title').text(errorData.caption))
	            .append($('<div>').addClass('card-text text-white mb-3').append(errorData.message))
	            .append($('<div>').addClass('card-text text-white')
	                .append('Informacje o systemie: ').append(platform.description))
	            .append($('<div>').addClass('card-text text-white')
	                .append('Wersja pluginu: ').append(GM_info.version))
	            .append($('<div>').addClass('card-text text-white')
	                .append('Wersja skryptu: ').append(GM_info.script.version));
	        card.append(cardHeader).append(cardBody);
	        return card;
	    };
	
	    DomTamper.removeButton = function(properties){
	        $(properties.injection.selector).find('#' + properties.injection.id).remove();
	    };
	
	    DomTamper.createButton = function(properties){
	        DomTamper.removeButton(properties);
	        var element = properties.inject();
	        element.bind('click', properties.click);
	        $(properties.injection.selector).append(element);
	    };
	
	    DomTamper.createLoader = function(w){
	        prepareHead(w);
	        var pageContent = $('<div>').addClass('page-content');
	        pageContent.append(createLoaderContent());
	        pageContent.append(createLinks(w, 'special-color white-text'));
	        prepareBody(w, pageContent);
	        Unloader.init(w);
	    };
	
	    var createLoaderContent = function(){
	        var card = $('<div>').addClass('card text-white bg-dark');
	        var cardHeader = $('<div>').addClass('card-header').text('Poczekaj trwa wczytywanie danych...');
	        var cardBody = $('<div>').addClass('card-body');
	        var bodyContainer = $('<div>').addClass('d-flex justify-content-center m-3');
	        var spinner = $('<div>').addClass('spinner-border spinner-size').attr('role', 'status')
	            .append($('<span>').addClass('sr-only').text('Loading...'));
	        cardBody.append(bodyContainer.append(spinner));
	        card.append(cardHeader).append(cardBody);
	
	        return card;
	    };
	
	    var setWindowTitle = function(data, w){
	        var head = $(w.document.head);
	        var title = head.find('title');
	        if(title.length) {
	            title.text(data.title);
	        }
	        else {
	            head.append($('<title>').text(data.title));
	        }
	    };
	
	    DomTamper.createDocument = function(data, w){
	        prepareHead(w);
	        setWindowTitle(data, w);
	        var pageContent = $('<div>').addClass('page-content');
	        pageContent.append(Accordion.create(w, data));
	        pageContent.append(createLinks(w, 'special-color white-text'));
	        pageContent.append(createNotificationContainer());
	        prepareBody(w, pageContent, true);
	        Unloader.init(w);
	        Accordion.bindActions(w, data);
	    };
	
	    var createNotificationContainer = function(){
	        return $('<div>').attr('id', 'notification-container')
	            .attr('aria-live', 'polite').attr('aria-atomic', 'true').addClass('notification-container');
	    };
	
	    return DomTamper;
	}(DomTamper || {}));
	
	var HistoryTamper = (function(HistoryTamper){
	    HistoryTamper.onLocationChange = function(locationChangeCallback){
	        history.pushState = ( f => function pushState(){
	            var ret = f.apply(this, arguments);
	            window.dispatchEvent(new Event('pushstate'));
	            window.dispatchEvent(new Event('locationchange'));
	            return ret;
	        })(history.pushState);
	
	        history.replaceState = ( f => function replaceState(){
	            var ret = f.apply(this, arguments);
	            window.dispatchEvent(new Event('replacestate'));
	            window.dispatchEvent(new Event('locationchange'));
	            return ret;
	        })(history.replaceState);
	
	        window.addEventListener('popstate',()=>{
	            window.dispatchEvent(new Event('locationchange'))
	        });
	
	        window.addEventListener('locationchange', function(){
	            locationChangeCallback();
	        });
	    };
	
	    return HistoryTamper;
	}(HistoryTamper || {}));
	
	var Accordion = (function(Accordion) {
	    Accordion.create = function(w, data){
	        var mainCardTitle = $('<div>').addClass('card-header').text(data.title);
	
	        var accordion = $('<div>').addClass('accordion md-accordion').attr('id', 'accordion')
	            .attr('role', 'tablist').attr('aria-multiselectable', 'true');
	
	        createCards(accordion, data);
	
	        var mainCardBody = $('<div>').addClass('card-body p-0').append(accordion);
	        return $('<div>').addClass('card').append(mainCardTitle).append(mainCardBody);
	    };
	
	    var createCards = function(accordion, data) {
	        for(var key in data.cards) {
	            var card = createCard({
	                card: data.cards[key],
	                key: key,
	                title: data.title
	            });
	            accordion.append(card);
	        }
	    };
	
	    var createCard = function(data){
	        var accordionCard = $('<div>').addClass('border border-top-0');
	        var content = $('<div>').addClass('card-body pt-0');
	
	        var badgeClass = 'badge-light';
	        var textMuted = 'text-muted';
	        if(data.card.items.length > 0){
	            badgeClass = 'badge-danger';
	            textMuted = 'text-dark';
	            content.append(createCardContent(data));
	        }
	
	        var icon = $('<i>').addClass('fas').addClass(data.card.icon).addClass('pr-2');
	        var badge = $('<span>').addClass('badge mr-3 float-right').addClass(badgeClass)
	            .text(data.card.items.length);
	        var cardTitle = $('<h6>').addClass('mb-0').addClass(textMuted).append(icon).append(badge)
	            .append($('<span>').text(data.card.label));
	        var link = $('<a>').append(cardTitle);
	        var cardHeader = $('<div>').addClass('ml-3 p-2').attr('role', 'tab').attr('id', data.key).append(link);
	
	        var cardBody = $('<div>').addClass('collapse').attr('role', 'tabpanel')
	            .attr('aria-labelledby', data.key).append(content);
	        if(data.card.collapse){
	            cardBody.addClass('show');
	        }
	
	        accordionCard.append(cardHeader);
	        accordionCard.append(cardBody);
	        return accordionCard;
	    };
	
	    var createCardContent = function(data){
	        var table = $('<table>').addClass('table table-bordered table-striped btn-table');
	        var tbody = $('<tbody>');
	        table.append(tbody);
	        createRows(tbody, data);
	
	        return table;
	    };
	
	    var createRows = function(tableBody, data){
	        data.card.items.forEach(function(item) {
	            tableBody.append(createRow({
	                item: item,
	                info: data.card.info,
	                title: data.title,
	                actions: data.card.actions
	            }));
	        });
	    };
	
	    var createRow = function(data){
	        var actions = $('<td>').attr('scope', 'row').addClass('action-row-' + data.actions.length);
	        data.actions.forEach(function(action){
	            actions.append(createButton(action, data));
	        });
	
	        var description = $('<td>').html(createDescriptionHtml(data));
	        return $('<tr>').append(actions).append(description);
	    };
	
	    var createDescriptionHtml = function(data){
	        var descriptionHtml = $('<div>');
	
	        createDescription(data).forEach(function(item, idx, array){
	            descriptionHtml.append($('<b>').text(item.desc + ': '))
	                .append($('<span>').text(item.value));
	            if(idx !== array.length - 1) {//not last
	                descriptionHtml.append($('<span>').text(', '));
	            }
	        });
	        return descriptionHtml;
	    };
	
	    var itemExist = function(data, info){
	        return data.item.hasOwnProperty(info.name) && data.item[info.name] != null
	    };
	
	    var createDescription = function(data){
	        var description = [];
	        data.info.forEach(function(info){
	            if (itemExist(data, info)) {
	                description.push({
	                    desc: info.desc,
	                    value: data.item[info.name]
	                });
	            }
	        });
	        return description;
	    };
	
	    var createButton = function(action, data){
	        return $('<button>').attr('type', 'button').attr('data-url', data.item.url).attr('data-title', data.title)
	            .addClass('btn btn-dark btn-sm m-1 pl-3 pr-3')
	            .append($('<i>').addClass('fas pr-1').addClass(action.icon)).append(action.label);
	    };
	
	    Accordion.bindActions = function(w, data){
	        cardActions(w, data);
	        buttonActions(w);
	    };
	
	    var cardActions = function(w, data){
	        for(var key in data.cards) {
	            var cardHeader = $(w.document.body).find('#' + key);
	            var disabled = cardHeader.find('h6.text-muted');
	            if(disabled.length){
	                disabled.addClass('cursor-normal');
	            }
	            else {
	                $(w.document.body).find('#' + key).click(function() {
	                    var id = $(this).attr('id');
	                    $(w.document.body).find('div[aria-labelledby="' + id + '"]').toggle();
	                });
	            }
	        }
	    };
	
	    var buttonActions = function(w){
	        getButton(w, '.fa-clone').click(function(){ copyActionClick($(this), w) });
	        getButton(w, '.fa-film').click(function(){ openActionClick($(this), w) });
	        getButton(w, '.fa-download').click(function(){ downloadActionClick($(this), w) });
	    };
	
	    var getButton = function(w, iconClass){
	        return $(w.document.body).find(iconClass).parent();
	    };
	
	    var downloadActionClick = function (element, w) {
	        var options = {title: 'Rozpoczęto pobieranie pliku', content: element.attr('data-title')};
	        Tool.downloadFile(element.attr('data-url'), element.attr('data-title'));
	        Notification.show(options, w);
	    };
	
	    var copyActionClick = function (element, w) {
	        GM_setClipboard(element.attr('data-url'));
	        var options = {title: 'Kopiowanie', content: 'Skopiowano do schowka'};
	        Notification.show(options, w);
	    };
	
	    var openActionClick = function (element, w) {
	        w.open(element.attr('data-url'));
	    };
	
	    return Accordion;
	}(Accordion || {}));
	
	var Executor = (function(Executor){
	    var execute = function(service, options, w){
	        var setup = setupStep(service, options);
	        logStepInfo(options, setup);
	        if(setup.isRemote()){
	             executeAsync(service, setup, options, w);
	        }
	        else {
	            callback(service, options, w);
	        }
	    };
	
	    var executeAsync = function(service, setup, options, w){
	        var chain = options.chainNames[options.chainIndex];
	        var chainStep = chain + '[' + options.stepIndex + ']';
	        var exceptionParams = [chainStep, Tool.getRealUrl()];
	        var requestParams = {
	            method: setup.method,
	            headers: setup.headers,
	            url: setup.resolveUrl.url,
	            data: JSON.stringify(setup.methodParam()),
	            responseType: setup.responseType,
	            onload: function(data) {
	                var currentStep = getCurrentStep(service, options);
	                if(retryPossible(currentStep, options, data.status)){
	                    execute(service, options, w);
	                }
	                else {
	                    if(setup.responseType === 'jsonp'){
	                        var match = data.responseText.match(/callback\(([\s\S]*?)\);/);
	                        if(match && match[1] && !match[1].startsWith('null')){
	                            setStepResult(options, {async: JSON.parse(match[1])});
	                        }
	                    }
	                    else {
	                        setStepResult(options, {async: data.response || {}});
	                    }
	                    callback(service, options, w);
	                }
	            },
	            onerror: function(data){
	                DomTamper.handleError(new Exception(config.error.call, exceptionParams), w);
	            },
	            ontimeout: function(){
	                DomTamper.handleError(new Exception(config.error.timeout, exceptionParams), w);
	            }
	        };
	        GM_xmlhttpRequest(requestParams);
	    };
	
	    var retryPossible = function(step, options, status){
	        return step.retryErrorCodes.indexOf(status) >= 0 && step.urlTemplateParts[options.retries++];
	    };
	
	    var logStepInfo = function(options, setup){
	        var chain = options.chainNames[options.chainIndex];
	        var step = chain + '[' + options.stepIndex + ']';
	        var stepParams = $.isEmptyObject(setup.methodParam()) ? '' : JSON.stringify(setup.methodParam());
	        var params = [
	            'color:green', options.retries+1, 'color:black', ':',
	            'color:blue', step,  'color:red', setup.isRemote() ? setup.method : '---',
	            'color:black;font-weight: bold', setup.isRemote() ? setup.resolveUrl.url : '---', 'color:magenta', stepParams
	        ];
	        Tool.formatConsoleMessage('%c%s%c%s%c%s%c %s %c %s %c%s', params);
	    };
	
	    var setupStep = function(service, options){
	        var currentStep = getCurrentStep(service, options);
	        beforeStep(currentStep, options);
	        var setup = $.extend(true, {}, currentStep);
	        if(currentStep.isRemote()) {
	            setup.resolveUrl = currentStep.resolveUrl(getStepResult(options).before, options.retries);
	        }
	        return setup;
	    };
	
	    var getCurrentStep = function(service, options){
	        var chain = options.chainNames[options.chainIndex];
	        var steps = service.chains[chain];
	        return steps[options.stepIndex];
	    };
	
	    var beforeStep = function(currentStep, options){
	        var stepOutput = currentStep.before(getStepResult(options, true).after || {}, getStepResult(options, true));
	        if(currentStep.isRemote()){
	            if(typeof stepOutput === 'string' || typeof stepOutput == 'number'){
	                var result = stepOutput;
	                stepOutput = {};
	                stepOutput[config.urlParamDefaultKey] = result;
	            }
	        }
	        setStepResult(options, {before: stepOutput});
	    };
	
	    var getStepResult = function(options, previous){
	        var chain = options.chainNames[options.chainIndex];
	        if(!options.results){
	            options.results = {};
	        }
	        if(!options.results[chain]){
	            options.results[chain] = [];
	        }
	        if(!options.results[chain][options.stepIndex]){
	            options.results[chain].push({});
	        }
	        var stepIndex = previous && options.stepIndex > 0 ? options.stepIndex - 1 : options.stepIndex;
	        return options.results[chain][stepIndex];
	    };
	
	    var setStepResult = function(options, object){
	        var chain = options.chainNames[options.chainIndex];
	        options.results[chain][options.stepIndex] = $.extend(true, getStepResult(options), object);
	    };
	
	    var hasNextStep = function(service, options){
	        var chain = options.chainNames[options.chainIndex];
	        var steps = service.chains[chain];
	        return steps.length - 1 > options.stepIndex;
	    };
	
	    var hasNextChain = function(service, options){
	        return options.chainNames.length - 1 > options.chainIndex;
	    };
	
	    var pushChain = function(service, options){
	        if(hasNextChain(service, options)){
	            options.chainIndex += 1;
	            options.stepIndex = 0;
	            return true;
	        }
	        return false;
	    };
	
	    var pushStep = function(service, options) {
	        if(hasNextStep(service, options)){
	            options.stepIndex += 1;
	            return true;
	        }
	        return false;
	    };
	
	    var afterStep = function(service, options) {
	        var currentStep = getCurrentStep(service, options);
	        var previousResult = currentStep.isRemote() ? getStepResult(options).async : getStepResult(options).before;
	        var output = currentStep.after(previousResult || {}, getStepResult(options));
	        options.retries = 0;
	        setStepResult(options, {after: output});
	    };
	
	    var callback = function(service, options, w){
	        try {
	            afterStep(service, options);
	            if(pushStep(service, options) || pushChain(service, options)) {
	                return Promise.resolve().then(
	                    Executor.chain(service, options, w)
	                );
	            }
	            else {
	                return Promise.resolve().then(
	                    service.onDone(options.results, w)
	                );
	            }
	        }
	        catch(e){
	            DomTamper.handleError(e, w);
	        }
	    };
	
	    Executor.chain = function(service, options, w){
	        try {
	            if(w === undefined){
	                w = window.open();
	                DomTamper.createLoader(w, service);
	            }
	
	            execute(service, options, w);
	        }
	        catch(e){
	            DomTamper.handleError(e, w);
	        }
	    };
	
	    return Executor;
	}(Executor || {}));
	
	function Configurator(properties){
	    var service = {
	        observer: {
	            anchor: undefined,
	            mode: 'added',
	            selector: undefined
	        },
	        injection: {
	            selector: properties.observer.selector,
	            id: 'direct-download',
	            class: '',
	        },
	        cardsData: {
	            title: '',
	            cards: {
	                videos: {
	                    icon: 'fa-video', label: 'Video', collapse: true, items: [],
	                    info: [
	                        {name: 'video', desc: 'video'},
	                        {name: 'audio', desc: 'audio'},
	                        {name: 'language', desc: 'wersja językowa'}
	                    ],
	                    actions: [
	                        {label: 'Pobierz', icon: 'fa-download'},
	                        {label: 'Kopiuj', icon: 'fa-clone'},
	                        {label: 'Otwórz', icon: 'fa-film'}
	                    ]
	                },
	                subtitles: {
	                    icon: 'fa-file-alt', label: 'Napisy', collapse: false, items: [],
	                    info: [
	                        {name: 'description', desc: 'opis'},
	                        {name: 'format', desc: 'format'}
	                    ],
	                    actions: [
	                        {label: 'Pobierz', icon: 'fa-download'}
	                    ]
	                },
	                streams: {
	                    icon: 'fa-stream', label: 'Strumienie', collapse: false, items: [],
	                    info: [
	                        {name: 'description', desc: 'opis'},
	                        {name: 'format', desc: 'format'}
	                    ],
	                    actions: [
	                        {label: 'Kopiuj', icon: 'fa-clone'}
	                    ]
	                }
	            }
	        },
	        chains: {
	            videos: []
	        },
	        chainSelector: function(){
	            return ['videos'];
	        },
	        formatter: function(data){
	            data.cards['videos'].items.sort(function (a, b) {
	                return a.index - b.index;
	            });
	            data.cards['subtitles'].items.sort(function (a, b) {
	                return ('' + a.format).localeCompare(b.format);
	            });
	            data.cards['streams'].items.sort(function (a, b) {
	                return ('' + a.format).localeCompare(b.format);
	            });
	        },
	        aggregate: function(data){
	            var aggregatedData = $.extend(true, {}, service.cardsData);
	            var chains = service.chainSelector();
	            chains.forEach(function(chain){
	                var extend = data[chain][data[chain].length - 1].after;
	                $.extend(true, aggregatedData, extend);
	            });
	            return aggregatedData;
	        },
	        onDone: function(data, w) {
	            var aggregatedData = service.aggregate(data);
	            service.formatter(aggregatedData);
	            DomTamper.createDocument(aggregatedData, w);
	        },
	        ready: function(){
	            return $(service.observer.selector).length > 0;
	        },
	        click: function(){
	            var chainNames = service.chainSelector();
	            Executor.chain(service, {
	                stepIndex: 0,
	                chainIndex: 0,
	                retries: 0,
	                chainNames: chainNames
	            });
	        },
	        inject: function(){
	            var icon1 = $('<i>').addClass('fa fa-circle fa-stack-2x video_button_circle');
	            var icon2 = $('<i>').addClass('fa fa-video fa-stack-1x fa-inverse');
	            var span = $('<span>').addClass('fa-stack fa-1x').append(icon1).append(icon2);
	            var div = $('<div>')
	                .attr('id', service.injection.id).attr('title', 'informacje o wideo')
	                .append(span).addClass('video_button')
	                .addClass(service.injection.class);
	            $(service.observer.selector).hover(() => div.show(), () => div.hide());
	            return div;
	        },
	    };
	
	    return $.extend(true, service, properties);
	}
	
	var Detector = (function(conf) {
	    var configuration = conf;
	
	    var logObservation = function(){
	        var observer = configuration.properties.observer;
	        var color = configuration.properties.ready() ? 'color:green' : 'color:red';
	        var anchor = observer.anchor ? observer.anchor + '->' : '';
	        var params = [color, anchor + observer.selector, 'color:black'];
	        Tool.formatConsoleMessage('[%c%s%c]', params);
	    };
	
	    this.observe = function(){
	        var observer = configuration.properties.observer;
	        if(configuration.properties.ready()){
	            logObservation();
	            configuration.successCallback();
	        }
	        else {
	            $(observer.anchor).observe(observer.mode, observer.selector, function(record) {
	                logObservation();
	                configuration.successCallback();
	            });
	        }
	    };
	});
	
	var ElementDetector = (function(ElementDetector){
	    ElementDetector.detect = function(properties, callback){
	        var detector = new Detector({
	            properties: properties,
	            successCallback: callback
	        });
	        detector.observe();
	    };
	
	    return ElementDetector;
	}(ElementDetector || {}));
	
	var Unloader = (function(Unloader) {
	    var win;
	    var url;
	
	    Unloader.init = function(w){
	        win = w;
	        url = Tool.getRealUrl();
	        $(window).bind('beforeunload', function(){
	            if(!win.closed) {
	                DomTamper.handleError(new Exception(config.error.noParent, url), win);
	            }
	        });
	    };
	
	    return Unloader;
	}(Unloader || {}));
	
	var MessageReceiver = (function(MessageReceiver) {
	    var win;
	    var origin;
	    var callbackFunction;
	    var alreadyConfirmed = false;
	    var alreadyPosted = false;
	
	    var receiveMessage = function(event, callback){
	        if (event.origin !== origin) {
	            return;
	        }
	
	        var data = parseJSON(event.data);
	        if($.isEmptyObject(data)){
	            return;
	        }
	        if(data.confirmation){
	            alreadyConfirmed = true;
	        }
	        else {
	            data.confirmation = true;
	            if(!alreadyPosted) {
	                window.removeEventListener('message', callbackFunction);
	                alreadyPosted = true;
	                postMessage(data);
	                callback(data);
	            }
	        }
	    };
	
	    var parseJSON = function(json) {
	        if (typeof json == 'object')
	            return {};
	        try {
	            return JSON.parse(json);
	        }
	        catch(e) {
	            return {};
	        }
	    };
	
	    var postMessage = function(data){
	        data = JSON.stringify(data);
	        win.postMessage(data, '*');
	    };
	
	    MessageReceiver.awaitMessage = function(object, callback){
	        initCommunication(object, callback);
	    };
	
	    var initCommunication = function(object, callback){
	        callbackFunction = function(e){
	            receiveMessage(e, callback);
	        };
	        window.addEventListener('message', callbackFunction);
	        win = getProperty(object, 'windowReference');
	        origin = getProperty(object, 'origin');
	    };
	
	    var getProperty = function(object, prop){
	        if(object.hasOwnProperty(prop)){
	            return object[prop];
	        }
	    };
	
	    MessageReceiver.postUntilConfirmed = function(object){
	        initCommunication(object);
	        isMessageConfirmed(config.attempts, getProperty(object, 'message'))
	    };
	
	    var isMessageConfirmed = function(attempt, message){
	        if (alreadyConfirmed || attempt <= 0) {
	            return Promise.resolve().then(function(){
	                window.removeEventListener('message', callbackFunction);
	                if(attempt <= 0){
	                    console.warn("Nie udało się przekazać adresu z okna głównego.");
	                }
	            });
	        } else if(attempt > 0){
	            attempt = attempt-1;
	            postMessage(message);
	            return Promise.resolve().then(
	                setTimeout(isMessageConfirmed, config.attemptTimeout, attempt, message)
	            );
	        }
	    };
	
	    return MessageReceiver;
	}(MessageReceiver || {}));
	
	var Common = (function(Common) {
	    Common.grabIplaSubtitlesData = function(data){
	        var items = [];
	        var subtitles = (((data.result || {}).mediaItem || {}).displayInfo || {}).subtitles || [];
	        subtitles.forEach(function(subtitle) {
	            items.push({
	                url: subtitle.src,
	                description: subtitle.name,
	                format: subtitle.format
	            })
	        });
	        return {
	            cards: {subtitles: {items: items}}
	        };
	    };
	
	    Common.run = function(properties){
	        HistoryTamper.onLocationChange(function () {
	            DomTamper.removeButton(properties);
	        });
	        ElementDetector.detect(properties, function () {
	            DomTamper.createButton(properties);
	        });
	    };
	
	    Common.createProperties = function(anchor, selector, mode) {
	        return {
	            observer: {
	                anchor: anchor,
	                mode: mode ? mode : 'added',
	                selector: selector,
	            },
	            ready: function() {
	                return $(this.observer.selector).length > 0;
	            }
	        };
	    };
	
	    return Common;
	}(Common || {}));
	
	var TVP = (function() {
	    var dataAttributeParser = function() {
	        var src = $(properties.observer.selector).attr('data-video-id');
	        if(src !== undefined){
	            return {
	                videoId: src.split("/").pop()
	            };
	        }
	
	        return urlForwardParser();
	    };
	
	    var urlForwardParser = function() {
	        var urlMatch = window.location.href.match(/^https?:\/\/.*\.tvp\..*\/(\d{6,})\/.*$/);
	        if(urlMatch && urlMatch[1]){
	            return urlMatch[1];
	        }
	
	        return urlParameterParser();
	    };
	    var urlParameterParser = function(){
	        var ids = [
	            Tool.getUrlParameter('ID', window.location.href),
	            Tool.getUrlParameter('object_id', window.location.href)
	        ];
	        var id = ids.find(nonNull);
	        if(id){
	            return id;
	        }
	
	        throw new Exception(config.error.id, window.location.href);
	    };
	
	    var nonNull = function (id) {
	        return id !== null;
	    };
	
	    var properties = new Configurator({
	        observer: {
	            selector: '#JS-TVPlayer2-Wrapper, .player-video-container, #tvplayer, #Player'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'https://tvp.pl/pub/stat/videofileinfo?video_id=#videoId',
	                    before: function (input) {
	                        return dataAttributeParser();
	                    },
	                    after: function (input, result) {
	                        return getRealVideoId(input, result.before.videoId);
	                    }
	                }),
	                new Step({
	                    urlTemplate: 'https://vod.tvp.pl/sess/TVPlayer2/api.php?id=#videoId&@method=getTvpConfig' +
	                        '&@callback=callback',
	                    responseType: 'jsonp',
	                    after: function(input){
	                        return grapVideoData(input);
	                    }
	                })
	            ]
	        }
	    });
	
	    var getRealVideoId = function(json, videoId){
	        var videoId = (json || {}).copy_of_object_id !== undefined ?
	            json.copy_of_object_id : videoId;
	        return {
	            videoId: videoId
	        };
	    };
	
	    var grapVideoData = function(data){
	        var items = [];
	        var subtitlesItems = [];
	        var info = ((data || {}).content || {}).info || {};
	        var files = ((data || {}).content || {}).files || [];
	        var subtitles = ((data || {}).content || {}).subtitles || [];
	        var files = removeUnsupportedVideoFormats(files);
	        if(files.length) {
	            files.forEach(function (file) {
	                var videoDesc = file.quality.bitrate;
	                items.push(Tool.mapDescription({
	                    source: 'TVP',
	                    key: videoDesc,
	                    video: videoDesc,
	                    url: file.url
	                }));
	            });
	            subtitles.forEach(function(subtitle) {
	                var extension = subtitle.type;
	                subtitlesItems.push({
	                    url: 'https:' + subtitle.url,
	                    format: extension,
	                    description: subtitle.lang
	                })
	            });
	
	            return {
	                title: (info.title != null ? info.title : '') + (info.subtitle != null ? ' ' + info.subtitle : ''),
	                cards: {
	                    videos: {items: items},
	                    subtitles: {items: subtitlesItems}
	                }
	            }
	        }
	        throw new Exception(config.error.noSource, window.location.href);
	    };
	
	    var removeUnsupportedVideoFormats = function(files){
	        var result = [];
	        files.forEach(function (file) {
	            if (file['type'] === 'any_native') {
	                result.push(file);
	            }
	        });
	        return result;
	    };
	
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	});
	
	var TVN = (function() {
	    var properties = new Configurator({
	        observer: {
	            anchor: 'body',
	            selector: 'div.cover-buttons > a[href="#"], #player-container, div.custom-alert-inner-wrapper'
	        },
	        injection: {
	            selector: 'div.right-side'
	        },
	        inject: function(){
	            var icon = $('<i>').addClass('fas fa-video');
	            var button = $('<div>')
	                .attr('id', properties.injection.id).attr('title', 'pobierz video')
	                .append(icon).addClass('btn btn-login');
	            return button;
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'http://player.pl/api/?platform=ConnectedTV&terminal=Panasonic&format=json' +
	                        '&authKey=064fda5ab26dc1dd936f5c6e84b7d3c2&v=3.1&m=getItem&id=#videoId',
	                    before: function(input){
	                        return idParser();
	                    },
	                    after: function(output) {
	                        return grabVideoData(output);
	                    }
	                })
	            ]
	        },
	    });
	
	    var idParser = function(){
	        var watchingNow = $('.watching-now').closest('.embed-responsive').find('.embed-responsive-item');
	        if(watchingNow.length > 0){
	            return watchingNow.attr('href').split(',').pop();
	        }
	
	        return episodeIdParser();
	    };
	
	    var episodeIdParser = function () {
	        var match = window.location.href.match(/odcinki,(\d+)\/.*,(\d+)/);
	        if(match && match[2]){
	            return match[2];
	        }
	
	        return serialIdParser();
	    };
	
	    var serialIdParser = function () {
	        var match = window.location.href.match(/odcinki,(\d+)/);
	        if(match && match[1]){
	            throw new Exception(config.error.tvnId, Tool.getRealUrl());
	        }
	
	        return vodIdParser();
	    };
	
	    var vodIdParser = function(){
	        var match = window.location.href.match(/,(\d+)/);
	        if(match && match[1]){
	            return match[1];
	        }
	
	        throw new Exception(config.error.tvnId, Tool.getRealUrl());
	    };
	
	    var grabVideoData = function(data){
	        var items = [];
	        var main = ((data.item || {}).videos || {}).main || {};
	        var video_content = main.video_content || {};
	        if(main.video_content_license_type !== 'WIDEVINE' && video_content && video_content.length > 0){
	            $.each(video_content, function( index, value ) {
	                items.push(Tool.mapDescription({
	                    source: 'TVN',
	                    key: value.profile_name,
	                    video: value.profile_name,
	                    url: value.url
	                }));
	            });
	
	            return {
	                title: getTitle(data),
	                cards: {videos: {items: items}}
	            }
	        }
	        throw new Exception(config.error.noSource, Tool.getRealUrl());
	    };
	
	    var getTitle = function(data){
	        var episode = data.item.episode ? 'E'+Tool.pad(data.item.episode, 3) : '';
	        var season = data.item.season != null ? 'S'+Tool.pad(data.item.season, 2) : '';
	        var serie_title = data.item.serie_title != null ? data.item.serie_title : '';
	        var episodeTitle = data.item.title ? ' ' + data.item.title : '';
	        var seasonAndEpisode = season + episode;
	
	        return serie_title + (seasonAndEpisode !== '' ? ' - ' + seasonAndEpisode : '') +
	            (episodeTitle !== '' ? ' - ' + episodeTitle : '');
	    };
	
	    var inVodFrame = function(){
	        var regexp = new RegExp('https:\/\/player\.pl(.*)');
	        var match = regexp.exec(window.location.href);
	        if(match[1]) {
	            window.sessionStorage.setItem(config.storage.topWindowLocation, 'https://vod.pl' + match[1]);
	        }
	    };
	
	    this.setup = function(){
	        if(!Tool.isTopWindow() && $('#app').length) {
	            inVodFrame();
	        }
	
	        Common.run(properties);
	    };
	});
	
	var IPLA = (function() {
	    var properties = new Configurator({
	        observer: {
	            anchor: 'app-root',
	            selector: 'div.player-wrapper, div.promo-box:visible, div.player-error-presentation:visible'
	        },
	        chainSelector: function(){
	            return ['videos', 'subtitles'];
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplateParts: [
	                      'ua=www_iplatv_html5/12345',
	                      'ua=mipla_ios/122'
	                    ],
	                    urlTemplate: 'https://getmedia.redefine.pl/vods/get_vod/?cpid=1&~&media_id=#videoId',
	                    retryErrorCodes: [404],
	                    before: function (input) {
	                        return grabVideoIdFromUrl();
	                    },
	                    after: function(data){
	                        return grabVideoData(data);
	                    }
	                })
	            ],
	            subtitles: [
	                new Step({
	                    urlTemplate: 'https://b2c.redefine.pl/rpc/navigation/',
	                    method: 'POST',
	                    methodParam: function(){
	                        return getParamsForSubtitles();
	                    },
	                    after: Common.grabIplaSubtitlesData
	                })
	            ]
	        }
	    });
	
	    var grabVideoData = function(data){
	        var items = [];
	        var vod = data.vod || {};
	        if(vod.copies && vod.copies.length > 0 && !vod.drm){
	            $.each(vod.copies, function( index, value ) {
	                var videoDesc = value.quality_p + ', ' + value.bitrate;
	                items.push(Tool.mapDescription({
	                    source: 'IPLA',
	                    key: value.quality_p,
	                    video: videoDesc,
	                    url: value.url
	                }));
	            });
	            return {
	                title: vod.title,
	                cards: {videos: {items: items}}
	            }
	        }
	        throw new Exception(config.error.noSource, Tool.getRealUrl());
	    };
	
	    var getParamsForSubtitles = function(){
	        var mediaId = grabVideoIdFromUrl();
	        return {
	            jsonrpc: "2.0",
	            id: 1,
	            method: "prePlayData",
	            params: {
	                userAgentData: {
	                    application: "firefox",
	                    portal: "ipla"
	                },
	                cpid: 1,
	                mediaId: mediaId
	            }
	        }
	    };
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	
	    var matchingId = function(input, failureAction){
	        input = input ? input : '';
	        var match = matchingHexId(input);
	        if(!match){
	            match = matchingDecId(input);
	        }
	        return match ? match : failureAction();
	    };
	
	    var matchingHexId = function(input){
	        var match = input.match(/[0-9a-f]{32}/);
	        if(match && match[0]) {
	            return match[0];
	        }
	
	        return null;
	    };
	
	    var matchingDecId = function(input) {
	        var match = input.match(/([\d]+)?(\?.*)$/);
	        if(match && match[1]) {
	            return match[1];
	        }
	
	        return null;
	    };
	
	    var grabVideoIdFromUrl = function(){
	        return matchingId(location.href, grabVideoIdFromWatchingNowElement);
	    };
	
	    var grabVideoIdFromWatchingNowElement = function(){
	        return matchingId($('div.vod-image-wrapper__overlay').closest('a').attr('href'), grabVideoIdFromHtmlElement);
	    };
	
	    var grabVideoIdFromHtmlElement = function(){
	        var frameSrc = $('app-commercial-wallpaper iframe:first-child').attr('src');
	        if(frameSrc !== undefined) {
	            return Tool.getUrlParameter('vid', frameSrc);
	        }
	        throw new Exception(config.error.id, Tool.getRealUrl());
	    };
	});
	
	var VOD = (function() {
	    var properties = new Configurator({
	        observer: {
	            selector: '#v_videoPlayer'
	        },
	        injection: {
	            class: 'right_margin'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'https://player-api.dreamlab.pl/?body[id]=#videoId&body[jsonrpc]=2.0' +
	                        '&body[method]=get_asset_detail&body[params][ID_Publikacji]=#videoId' +
	                        '&body[params][Service]=vod.onet.pl&content-type=application/jsonp' +
	                        '&x-onet-app=player.front.onetapi.pl&callback=',
	                    before: function (input) {
	                        return idParser();
	                    },
	                    after: function (output) {
	                        return grabVideoData(output);
	                    }
	                })
	            ]
	        }
	    });
	
	    var idParser = function () {
	        var id = $(".mvp").attr('id');
	        if(id !== undefined){
	            return id.match(/mvp:(.+)/)[1];
	        }
	
	        return parseFromJS();
	    };
	
	    var parseFromJS = function(){
	        var scripts = $('script[type="text/javascript"]').filter(':not([src])');
	        for (var i = 0; i < scripts.length; i++) {
	            var match = $(scripts[i]).text().match(/\"mvpId\"\s*:\s*\"(\d+\.\d+)\"/);
	            if(match && match[1]){
	                return match[1];
	            }
	        }
	
	        throw new Exception(config.error.id, Tool.getRealUrl());
	    };
	
	    var grabVideoData = function (data) {
	        var items = [];
	        var subtitlesItems = [];
	        var video = (((data.result || new Array())[0] || {}).formats || {}).wideo || {};
	        var meta = ((data.result || new Array())[0] || {}).meta || {};
	        var subtitles = meta.subtitles || [];
	        var videoData = video['mp4-uhd'] && video['mp4-uhd'].length > 0 ? video['mp4-uhd'] : video['mp4'];
	        if(videoData && videoData.length > 0){
	            videoData.forEach(function(value) {
	                var videoDesc = value.vertical_resolution + ', ' + value.video_bitrate;
	                items.push(Tool.mapDescription({
	                    source: 'VOD',
	                    key: value.vertical_resolution,
	                    video: videoDesc,
	                    url: value.url
	                }));
	            });
	
	            subtitles.forEach(function(subtitle) {
	                var extension = subtitle.name.split('.').pop();
	                subtitlesItems.push({
	                    url: subtitle.url,
	                    format: extension,
	                    description: subtitle.name
	                })
	            });
	
	            return {
	                title: meta.title,
	                cards: {
	                    videos: {items: items},
	                    subtitles: {items: subtitlesItems}
	                }
	            }
	        }
	        throw new Exception(config.error.noSource, Tool.getRealUrl());
	    };
	
	    var iplaDetected = function(){
	        return $('#v_videoPlayer div.pulsembed_embed').length > 0;
	    };
	
	    var workWithSubService = function(){
	        var src = 'https://pulsembed.eu';
	        var frameSelector = 'iframe[src^="' + src + '"]';
	        var properties = Common.createProperties('div.pulsembed_embed', frameSelector);
	
	        ElementDetector.detect(properties, function () {
	            MessageReceiver.postUntilConfirmed({
	                windowReference: $(frameSelector).get(0).contentWindow,
	                origin: src,
	                message: {
	                    location: window.location.href
	                }
	            });
	        });
	    };
	
	    this.setup = function(){
	        if(iplaDetected()) {
	            workWithSubService();
	        }
	        else if(Tool.isTopWindow()){
	            Common.run(properties);
	        }
	    };
	});
	
	var VOD_IPLA = (function() {
	    var properties = new Configurator({
	        observer: {
	            anchor: 'body',
	            selector: '#player-wrapper, #playerContainer'
	        },
	        injection: {
	            class: 'left_margin'
	        },
	        chainSelector: function(){
	            return ['videos', 'subtitles'];
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'https://distro.redefine.pl/partner_api/v1/2yRS5K/media/#media_id/vod/player_data?' +
	                        'dev=pc&os=linux&player=html&app=firefox&build=12345',
	                    before: function (input) {
	                        return {media_id: idParser()};
	                    },
	                    after: function(data){
	                        return grabVideoData(data);
	                    }
	                })
	            ],
	            subtitles: [
	                new Step({
	                    after: function (input) {
	                        return Common.grabIplaSubtitlesData(getJson());
	                    }
	                })
	            ]
	        }
	    });
	
	    var grabVideoData = function(data){
	        var items = [];
	        var displayInfo = (data.mediaItem || {}).displayInfo || {};
	        var mediaSources = ((data.mediaItem || {}).playback || {}).mediaSources || {};
	        var videos = $.grep(mediaSources, function(source) {
	            return source.accessMethod === 'direct';
	        });
	        if(videos && videos.length > 0){
	            $.each(videos, function( index, value ) {
	                items.push(Tool.mapDescription({
	                    source: 'IPLA',
	                    key: value.quality,
	                    video: value.quality,
	                    url: value.url
	                }));
	            });
	            return {
	                title: displayInfo.title,
	                cards: {videos: {items: items}}
	            }
	        }
	        throw new Exception(config.error.noSource, Tool.getRealUrl());
	    };
	
	    var getJson = function(){
	        var match = $('script:not(:empty)').text().match(/(window\.CP\.embedSetup\()(.*)\);/);
	        if(match) {
	            var jsonObject = JSON.parse(match[2]);
	            return JSON.parse(jsonObject[0].media);
	        }
	
	        return {};
	    };
	
	    var idParser = function(){
	        try {
	            if($('#player-wrapper').length > 0) {
	                return (((getJson() || {}).result || {}).mediaItem || {}).id;
	            }
	            else if($('#playerContainer').length > 0){
	                return getMediaId();
	            }
	        }
	        catch(e){
	            throw new Exception(config.error.id, Tool.getRealUrl());
	        }
	    };
	
	    var getMediaId = function(){
	        var match = $('script:not(:empty)').text().match(/mediaId: "(\w+)",/);
	        return match[1];
	    };
	
	    this.setup = function(){
	        var callback = function(data) {
	            window.sessionStorage.setItem(config.storage.topWindowLocation, data.location);
	            Common.run(properties);
	        };
	        MessageReceiver.awaitMessage({
	            origin: 'https://pulsembed.eu',
	            windowReference: window.parent
	        }, callback);
	    };
	});
	
	var WP = (function() {
	    var properties = new Configurator({
	        observer: {
	            anchor: 'body',
	            selector: 'div.npp-container'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'https://wideo.wp.pl/player/mid,#videoId,embed.json',
	                    before: function (input) {
	                        return idParser();
	                    },
	                    after: function (output) {
	                        return grabVideoData(output);
	                    }
	                })
	            ]
	        }
	    });
	
	    var idParser = function () {
	        try {
	            var id = window.location.href.match(/^(.*)-(\d+)v$/)[2];
	            //__NEXT_DATA__ is a variable on page
	            return __NEXT_DATA__.props.initialPWPState.material[id].mid;
	        }
	        catch(e){
	            throw new Exception(config.error.id, window.location.href);
	        }
	    };
	
	    var grabVideoData = function(data){
	        var items = [];
	        var urls = (data.clip || {}).url || {};
	        if(urls && urls.length > 0){
	            $.each(urls, function( index, value ) {
	                if(value.type === 'mp4@avc'){
	                    var videoDesc = value.quality + ', ' + value.resolution;
	                    items.push(Tool.mapDescription({
	                        source: 'WP',
	                        key: value.quality,
	                        video: videoDesc,
	                        url: value.url
	                    }));
	                }
	            });
	            return {
	                title: data.clip.title,
	                cards: {videos: {items: items}}
	            }
	        }
	        throw new Exception(config.error.noSource, window.location.href);
	    };
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	});
	
	var CDA = (function() {
	    var properties = new Configurator({
	        observer: {
	            selector: '.pb-player-content'
	        },
	        injection: {
	            class: 'right_margin'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    before: function(input){
	                        return getDestinationUrl();
	                    },
	                    after: function (input) {
	                        return grabVideoData(input);
	                    }
	                })
	            ]
	        }
	    });
	
	    var getDestinationUrl = function(){
	        var url = $("video.pb-video-player").attr('src');
	        if(url !== undefined){
	            if(!url.match(/blank\.mp4/)){
	                return url;
	            }
	            else if(l !== undefined){
	                return l;
	            }
	        }
	        throw new Exception(config.error.id, window.location.href);
	    };
	
	    var grabVideoData = function(data){
	        var items = [];
	        var title = $('meta[property="og:title"]');
	        var quality = $('.quality-btn-active');
	        var videoDesc = quality.length > 0 ? quality.text() : '-';
	        items.push(Tool.mapDescription({
	            source: 'CDA',
	            key: videoDesc,
	            video: videoDesc,
	            audio: '-',
	            url: data
	        }));
	        return {
	            title: title.length > 0 ? title.attr('content').trim() : 'brak danych',
	            cards: {videos: {items: items}}
	        };
	    };
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	});
	
	var NINATEKA = (function() {
	    var properties = new Configurator({
	        observer: {
	            selector: '#videoPlayer, #player'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    before: function(input){
	                        return getVideoUrls();
	                    },
	                    after: function (input) {
	                        return grabVideoData(input);
	                    }
	                })
	            ]
	        }
	    });
	
	    var grabVideoData = function(sources){
	        var videoItems = [];
	        var streamItems = [];
	        var title = $('meta[name="title"]').attr('content').trim();
	        if(sources && sources.length > 0){
	            $.each(sources, function(i, v ) {
	                if(sources[i].type && sources[i].type.match(/mp4/g)){
	                    videoItems.push(Tool.mapDescription({
	                        source: 'NINATEKA',
	                        key: v.type,
	                        url: v.src
	                    }));
	                }
	                else if(sources[i].type && (sources[i].type.match(/dash\+xml/g) || sources[i].type.match(/mpegURL/g))){
	                    streamItems.push(Tool.mapDescription({
	                        source: 'NINATEKA',
	                        key: v.type,
	                        url: v.src
	                    }));
	                }
	            });
	            return {
	                title: title.length > 0 ? title : 'brak danych',
	                cards: {
	                    videos: {items: videoItems},
	                    streams: {items: streamItems}
	                }
	            }
	        }
	        throw new Exception(config.error.noSource, window.location.href);
	    };
	
	    var getVideoUrls = function(){
	        var videoPlayer = $('#videoPlayer').data('player-setup');
	        var sources = (videoPlayer || {}).sources || [];
	        if(sources.length == 0){
	            var scripts = $('script[type="text/javascript"]').filter(':not([src])');
	            for (var i = 0; i < scripts.length; i++) {
	                var match = $(scripts[i]).text().match(/fn_\S+\(playerOptionsWithMainSource,\s*\d+\)\.sources/g);
	                if(match && match[0]){
	                    sources = eval(match[0]);
	                    break;
	                }
	            }
	        }
	        return sources;
	    };
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	});
	
	var ARTE = (function() {
	    var properties = new Configurator({
	        observer: {
	            anchor: 'div.video-thumbnail',
	            selector: 'div.avp-player'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'https://api.arte.tv/api/player/v1/config/#langCode/#videoId',
	                    before: function (input) {
	                        return idParser();
	                    },
	                    after: function (output) {
	                        return grabVideoData(output);
	                    }
	                })
	            ]
	        },
	        formatter: function(data) {
	            data.cards['videos'].items.sort(function (a, b) {
	                return a.index - b.index;
	            });
	
	            var sortingOrder = {'POL': 1};
	            data.cards['videos'].items.sort(function (a, b) {
	                var aLangOrder = sortingOrder[a.langCode] ? sortingOrder[a.langCode] : -1,
	                    bLangOrder = sortingOrder[b.langCode] ? sortingOrder[b.langCode] : -1;
	                return bLangOrder - aLangOrder;
	
	            });
	        }
	    });
	
	    var detectLanguage = function() {
	        var regexp = new RegExp('https:\/\/www.arte\.tv\/(\\w{2})\/');
	        var match = regexp.exec(window.location.href);
	        return match[1];
	    };
	
	    var detectVideoId = function(){
	        var regexp = new RegExp('https:\/\/www.arte\.tv\/\\w{2}\/videos\/([\\w-]+)\/');
	        var match = regexp.exec(window.location.href);
	        return match[1];
	    };
	
	    var idParser = function() {
	        try {
	            return {
	                videoId: detectVideoId(),
	                langCode: detectLanguage()
	            };
	        }
	        catch(e){
	            throw new Exception(config.error.id, window.location.href);
	        }
	    };
	
	    var grabVideoData = function(data){
	        var items = [];
	        var title = (((data || {}).videoJsonPlayer || {}).eStat || {}).streamName || '';
	        var streams = ((data || {}).videoJsonPlayer || {}).VSR || {};
	        if(streams){
	            Object.keys(streams).filter(function(k, i) {
	                return k.startsWith("HTTPS");
	            }).forEach(function(k) {
	                var stream = streams[k];
	                var videoDesc = stream.width + 'x' + stream.height + ', ' + stream.bitrate;
	                items.push(Tool.mapDescription({
	                    source: 'ARTE',
	                    key: stream.bitrate,
	                    video: videoDesc,
	                    langCode: stream.versionShortLibelle,
	                    language: stream.versionLibelle,
	                    url: stream.url
	                }));
	            });
	            return {
	                title: title,
	                cards: {videos: {items: items}}
	            }
	        }
	        throw new Exception(config.error.noSource, window.location.href);
	    };
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	
	});
	
	var TV_TRWAM = (function() {
	    var properties = new Configurator({
	        observer: {
	            anchor: '#ipott',
	            mode: 'removed',
	            selector: 'div[data-name="playerWindowPlace"]'
	        },
	        injection: {
	            class: 'left_margin'
	        },
	        chains: {
	            videos: [
	                new Step({
	                    urlTemplate: 'https://api-trwam.app.insysgo.pl/v1/Tile/GetTiles',
	                    headers: {'Content-Type': 'application/json'},
	                    method: 'POST',
	                    methodParam: function(){
	                        return getParamsForVideo();
	                    },
	                    after: function(json) {
	                        return getCodename(json);
	                    }
	                }),
	                new Step({
	                    urlTemplate: 'https://api-trwam.app.insysgo.pl/v1/Player/AcquireContent?platformCodename=www&' +
	                        'codename=#codename',
	                    after: function(output) {
	                        return grabData(output);
	                    }
	                })
	            ]
	        }
	    });
	
	    var grabVideoIdFromUrl = function(input){
	        var match = input.match(/\/(vod\.[\d]+)$/);
	        if(match && match[1]) {
	            return match[1];
	        }
	
	        throw new Exception(config.error.id, Tool.getRealUrl());
	    };
	
	    var getParamsForVideo = function(){
	        var mediaId = grabVideoIdFromUrl(window.location.href);
	        return {
	            platformCodename: "www",
	            tilesIds:[mediaId]
	        }
	    };
	
	    var getCodename = function(json){
	        var tile = (json.Tiles || [])[0] || {};
	        return {
	            title: tile.Title || {},
	            codename: tile.Codename || {}
	        };
	    };
	
	    var grabData = function(data){
	        var streams = (((data || {}).MediaFiles || [])[0] || {}).Formats || [];
	        var videoItems = grabVideoData(streams);
	        var streamItems = grabStreamData(streams);
	        if(videoItems.length > 0 || streamItems.length > 0){
	            return {
	                cards: {
	                    videos: {items: videoItems},
	                    streams: {items: streamItems}
	                }
	            }
	        }
	        throw new Exception(config.error.noSource, window.location.href);
	    };
	
	    var grabVideoData = function(streams){
	        var items = [];
	        $.each(streams, function( index, value ) {
	            if(value.Type === 3){
	                items.push(Tool.mapDescription({
	                    source: 'TRWAM',
	                    key: value.Type,
	                    url: value.Url
	                }));
	            }
	        });
	        return items;
	    };
	
	    var grabStreamData = function(streams){
	        var items = [];
	        var types = [2, 9];
	        $.each(streams, function( index, value ) {
	            if($.inArray(value.Type, types) > -1){
	                items.push(Tool.mapDescription({
	                    source: 'TRWAM',
	                    key: value.Type,
	                    url: value.Url
	                }));
	            }
	        });
	        return items;
	    }
	
	    this.setup = function(){
	        Common.run(properties);
	    };
	
	});
	
	var VOD_FRAME = (function() {
	    this.setup = function(){
	        var callback = function(data) {
	            var srcArray = ['https://redir.atmcdn.pl', 'https://partner.ipla.tv'];
	            setupDetector(srcArray, data);
	        };
	        MessageReceiver.awaitMessage({
	            origin: 'https://vod.pl',
	            windowReference: window.parent
	        }, callback);
	    };
	
	    var setupDetector = function(srcArray, data){
	        var selectors = createArrySelectors(srcArray);
	        var multiSelector = createMultiSelector(selectors);
	        var properties = Common.createProperties('div.iplaContainer', multiSelector);
	
	        ElementDetector.detect(properties, function() {
	            selectors.forEach(function(element){
	                if($(element.frameSelector).length > 0){
	                    MessageReceiver.postUntilConfirmed({
	                        windowReference: $(element.frameSelector).get(0).contentWindow,
	                        origin: element.src,
	                        message: {
	                            location: data.location
	                        }
	                    });
	                }
	            });
	        });
	    };
	
	    var createArrySelectors = function(srcArray){
	        return jQuery.map(srcArray, function(src) {
	            return {
	                src: src,
	                frameSelector: 'iframe[src^="' + src + '"]'
	            }
	        });
	    };
	
	    var createMultiSelector = function(selectors){
	        return $.map(selectors, function(src){
	            return src.frameSelector
	        }).join(', ');
	    }
	});
	
	var Starter = (function(Starter) {
	    var sources = [
	        {objectName: 'TVP', urlPattern: new RegExp(
	                '^https:\/\/(vod|cyfrowa)\.tvp\.pl\/video\/.*$|' +
	                '^https?:\/\/.*\.tvp\.(pl|info)\/sess\/TVPlayer2\/embed.*$|' +
	                '^https?:\/\/((?!wiadomosci).)*\.tvp\.pl\/\\d{6,}\/.*$|' +
	                '^https?:\/\/w{3}\.tvpparlament\.pl\/sess\/.*'
	            )
	        },
	        {objectName: 'TVN', urlPattern: new RegExp('^https:\/\/(?:w{3}\.)?(?:tvn)?player\.pl\/')},
	        {objectName: 'CDA', urlPattern: new RegExp('^https:\/\/.*\.cda\.pl\/')},
	        {objectName: 'VOD', urlPattern: new RegExp('^https:\/\/vod.pl\/')},
	        {objectName: 'VOD_IPLA', urlPattern: new RegExp(
	                '^https:\/\/partner\.ipla\.tv\/embed\/|' +
	                '^https:\/\/.*\.redcdn\.pl\/file\/o2\/redefine\/partner\/'
	            )
	        },
	        {objectName: 'IPLA', urlPattern: new RegExp('^https:\/\/w{3}\.ipla\.tv\/')},
	        {objectName: 'WP', urlPattern: new RegExp('^https:\/\/wideo\.wp\.pl\/')},
	        {objectName: 'NINATEKA', urlPattern: new RegExp('^https:\/\/ninateka.pl\/')},
	        {objectName: 'ARTE', urlPattern: new RegExp('^https:\/\/w{3}\.arte\.tv\/.*\/videos\/')},
	        {objectName: 'VOD_FRAME', urlPattern: new RegExp('^https:\/\/pulsembed\.eu\/')},
	        {objectName: 'TV_TRWAM', urlPattern: new RegExp('^https:\/\/tv-trwam\.pl\/local-vods\/')}
	    ];
	
	    Starter.start = function() {
	        sources.some(function(source){
	            if(source.urlPattern.exec(location.href)){
	                console.info('voddownloader: context: ' + source.objectName + ', url: ' + location.href);
	                var object = eval('new ' + source.objectName + '()');
	                object.setup();
	                return true;
	            }
	        });
	    };
	
	    return Starter;
	}(Starter || {}));
	
	$(document).ready(function(){
	    DomTamper.injectStylesheet(window, config.include.fontawesome);
	    DomTamper.injectStyle(window, 'buttons_css');
	    Starter.start();
	});

}).bind(this)(jQuery, platform, Waves);