├── .gitignore ├── screenshots ├── tvn.png ├── vod.png ├── info.png └── ipla.png ├── src ├── css │ ├── buttons.css │ ├── sources.css │ └── content.css ├── static │ ├── template.js │ └── header.txt ├── attach.js ├── util │ ├── exception.js │ ├── elementDetector.js │ ├── unloader.js │ ├── detector.js │ ├── historyTamper.js │ ├── step.js │ ├── notification.js │ ├── pluginSettingsDetector.js │ ├── tool.js │ ├── messageReceiver.js │ ├── configurator.js │ ├── accordion.js │ ├── executor.js │ ├── config.js │ └── domTamper.js ├── source │ ├── common.js │ ├── vod_frame.js │ ├── cda.js │ ├── wp.js │ ├── ninateka.js │ ├── arte.js │ ├── vod_ipla.js │ ├── tv_trwam.js │ ├── ipla.js │ ├── vod.js │ ├── tvn.js │ └── tvp.js └── starter.js ├── lib └── css │ ├── voddownloader-buttons.css │ └── voddownloader-content.css ├── README.md ├── dist └── voddownloader.meta.js ├── example_links.txt ├── package.json ├── gulpfile.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | tmp/ 4 | package-lock.json -------------------------------------------------------------------------------- /screenshots/tvn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacny/voddownloader/HEAD/screenshots/tvn.png -------------------------------------------------------------------------------- /screenshots/vod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacny/voddownloader/HEAD/screenshots/vod.png -------------------------------------------------------------------------------- /screenshots/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacny/voddownloader/HEAD/screenshots/info.png -------------------------------------------------------------------------------- /screenshots/ipla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacny/voddownloader/HEAD/screenshots/ipla.png -------------------------------------------------------------------------------- /src/css/buttons.css: -------------------------------------------------------------------------------- 1 | /* common style for buttons */ 2 | .playerBox-x, .js-video { 3 | position: relative; 4 | } 5 | -------------------------------------------------------------------------------- /src/static/template.js: -------------------------------------------------------------------------------- 1 | (function vodDownloader($, platform, Waves) { 2 | 'use strict'; 3 | 4 | // @include@ 5 | 6 | }).bind(this)(jQuery, platform, Waves); -------------------------------------------------------------------------------- /src/attach.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | DomTamper.injectStylesheet(window, config.include.fontawesome); 3 | DomTamper.injectStyle(window, 'buttons_css'); 4 | Starter.start(); 5 | }); -------------------------------------------------------------------------------- /src/util/exception.js: -------------------------------------------------------------------------------- 1 | var Exception = (function(error, templateParams) { 2 | this.error = error; 3 | this.templateParams = Array.isArray(templateParams) ? templateParams : [templateParams]; 4 | }); 5 | -------------------------------------------------------------------------------- /src/static/header.txt: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ${data.name} 3 | // @version ${data.version} 4 | // @updateURL ${data.updateUrl} 5 | // @downloadURL ${data.downloadUrl} 6 | // @include@ 7 | // @resource buttons_css ${data.buttonsCssPath} 8 | // @resource content_css ${data.contentCssPath} 9 | // ==/UserScript== 10 | 11 | -------------------------------------------------------------------------------- /src/util/elementDetector.js: -------------------------------------------------------------------------------- 1 | var ElementDetector = (function(ElementDetector){ 2 | ElementDetector.detect = function(properties, callback){ 3 | var detector = new Detector({ 4 | properties: properties, 5 | successCallback: callback 6 | }); 7 | detector.observe(); 8 | }; 9 | 10 | return ElementDetector; 11 | }(ElementDetector || {})); 12 | -------------------------------------------------------------------------------- /src/util/unloader.js: -------------------------------------------------------------------------------- 1 | var Unloader = (function(Unloader) { 2 | var win; 3 | var url; 4 | 5 | Unloader.init = function(w){ 6 | win = w; 7 | url = Tool.getRealUrl(); 8 | $(window).bind('beforeunload', function(){ 9 | if(!win.closed) { 10 | DomTamper.handleError(new Exception(config.error.noParent, url), win); 11 | } 12 | }); 13 | }; 14 | 15 | return Unloader; 16 | }(Unloader || {})); 17 | -------------------------------------------------------------------------------- /src/css/sources.css: -------------------------------------------------------------------------------- 1 | /* styles of buttons for supported pages */ 2 | .video_button { 3 | cursor: pointer; 4 | position: absolute; 5 | top: 0px; 6 | font: 14px Arial, Helvetica, sans-serif; 7 | z-index: 5001; 8 | padding: 2px 2px !important; 9 | margin: 5px; 10 | } 11 | 12 | .video_button_circle { 13 | color: rgba(30,30,30,.7); 14 | } 15 | 16 | .right_margin { 17 | right: 5px; 18 | top: 5px !important; 19 | } 20 | 21 | .left_margin { 22 | left: 5px; 23 | top: 5px !important; 24 | } -------------------------------------------------------------------------------- /lib/css/voddownloader-buttons.css: -------------------------------------------------------------------------------- 1 | /* styles of buttons for supported pages */ 2 | .video_button { 3 | cursor: pointer; 4 | position: absolute; 5 | top: 0px; 6 | font: 14px Arial, Helvetica, sans-serif; 7 | z-index: 5001; 8 | padding: 2px 2px !important; 9 | margin: 5px; 10 | } 11 | 12 | .video_button_circle { 13 | color: rgba(30,30,30,.7); 14 | } 15 | 16 | .right_margin { 17 | right: 5px; 18 | top: 5px !important; 19 | } 20 | 21 | .left_margin { 22 | left: 5px; 23 | top: 5px !important; 24 | } 25 | /* common style for buttons */ 26 | .playerBox-x, .js-video { 27 | position: relative; 28 | } 29 | -------------------------------------------------------------------------------- /src/css/content.css: -------------------------------------------------------------------------------- 1 | /** common **/ 2 | .page-content { 3 | margin: 0.5rem; 4 | font-size: 14px; 5 | padding-bottom: 2.8rem; 6 | } 7 | 8 | .do-not-display { 9 | display: none; 10 | } 11 | 12 | .links-position { 13 | position: fixed; 14 | right: 0.25rem; 15 | bottom: 0.25rem; 16 | } 17 | 18 | /** loader **/ 19 | .spinner-size { 20 | width: 4rem; 21 | height: 4rem; 22 | } 23 | 24 | /** results **/ 25 | .notification-container { 26 | position: absolute; 27 | top: 0.33rem; 28 | right: 0.33rem; 29 | min-height: 10rem; 30 | } 31 | 32 | .action-row-3 { 33 | width: 20rem; 34 | text-align: center; 35 | } 36 | 37 | .action-row-1 { 38 | width: 9rem; 39 | text-align: center; 40 | } 41 | 42 | .notification { 43 | min-width: 22rem; 44 | } 45 | 46 | .notification-body { 47 | word-wrap: break-word; 48 | } 49 | 50 | .cursor-normal { 51 | cursor: default !important; 52 | } 53 | -------------------------------------------------------------------------------- /lib/css/voddownloader-content.css: -------------------------------------------------------------------------------- 1 | /** common **/ 2 | .page-content { 3 | margin: 0.5rem; 4 | font-size: 14px; 5 | padding-bottom: 2.8rem; 6 | } 7 | 8 | .do-not-display { 9 | display: none; 10 | } 11 | 12 | .links-position { 13 | position: fixed; 14 | right: 0.25rem; 15 | bottom: 0.25rem; 16 | } 17 | 18 | /** loader **/ 19 | .spinner-size { 20 | width: 4rem; 21 | height: 4rem; 22 | } 23 | 24 | /** results **/ 25 | .notification-container { 26 | position: absolute; 27 | top: 0.33rem; 28 | right: 0.33rem; 29 | min-height: 10rem; 30 | } 31 | 32 | .action-row-3 { 33 | width: 20rem; 34 | text-align: center; 35 | } 36 | 37 | .action-row-1 { 38 | width: 9rem; 39 | text-align: center; 40 | } 41 | 42 | .notification { 43 | min-width: 22rem; 44 | } 45 | 46 | .notification-body { 47 | word-wrap: break-word; 48 | } 49 | 50 | .cursor-normal { 51 | cursor: default !important; 52 | } 53 | -------------------------------------------------------------------------------- /src/util/detector.js: -------------------------------------------------------------------------------- 1 | var Detector = (function(conf) { 2 | var configuration = conf; 3 | 4 | var logObservation = function(){ 5 | var observer = configuration.properties.observer; 6 | var color = configuration.properties.ready() ? 'color:green' : 'color:red'; 7 | var anchor = observer.anchor ? observer.anchor + '->' : ''; 8 | var params = [color, anchor + observer.selector, 'color:black']; 9 | Tool.formatConsoleMessage('[%c%s%c]', params); 10 | }; 11 | 12 | this.observe = function(){ 13 | var observer = configuration.properties.observer; 14 | if(configuration.properties.ready()){ 15 | logObservation(); 16 | configuration.successCallback(); 17 | } 18 | else { 19 | $(observer.anchor).observe(observer.mode, observer.selector, function(record) { 20 | logObservation(); 21 | configuration.successCallback(); 22 | }); 23 | } 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /src/util/historyTamper.js: -------------------------------------------------------------------------------- 1 | var HistoryTamper = (function(HistoryTamper){ 2 | HistoryTamper.onLocationChange = function(locationChangeCallback){ 3 | history.pushState = ( f => function pushState(){ 4 | var ret = f.apply(this, arguments); 5 | window.dispatchEvent(new Event('pushstate')); 6 | window.dispatchEvent(new Event('locationchange')); 7 | return ret; 8 | })(history.pushState); 9 | 10 | history.replaceState = ( f => function replaceState(){ 11 | var ret = f.apply(this, arguments); 12 | window.dispatchEvent(new Event('replacestate')); 13 | window.dispatchEvent(new Event('locationchange')); 14 | return ret; 15 | })(history.replaceState); 16 | 17 | window.addEventListener('popstate',()=>{ 18 | window.dispatchEvent(new Event('locationchange')) 19 | }); 20 | 21 | window.addEventListener('locationchange', function(){ 22 | locationChangeCallback(); 23 | }); 24 | }; 25 | 26 | return HistoryTamper; 27 | }(HistoryTamper || {})); 28 | -------------------------------------------------------------------------------- /src/source/common.js: -------------------------------------------------------------------------------- 1 | var Common = (function(Common) { 2 | Common.grabIplaSubtitlesData = function(data){ 3 | var items = []; 4 | var subtitles = (((data.result || {}).mediaItem || {}).displayInfo || {}).subtitles || []; 5 | subtitles.forEach(function(subtitle) { 6 | items.push({ 7 | url: subtitle.src, 8 | description: subtitle.name, 9 | format: subtitle.format 10 | }) 11 | }); 12 | return { 13 | cards: {subtitles: {items: items}} 14 | }; 15 | }; 16 | 17 | Common.run = function(properties){ 18 | HistoryTamper.onLocationChange(function () { 19 | DomTamper.removeButton(properties); 20 | }); 21 | ElementDetector.detect(properties, function () { 22 | DomTamper.createButton(properties); 23 | }); 24 | }; 25 | 26 | Common.createProperties = function(anchor, selector, mode) { 27 | return { 28 | observer: { 29 | anchor: anchor, 30 | mode: mode ? mode : 'added', 31 | selector: selector, 32 | }, 33 | ready: function() { 34 | return $(this.observer.selector).length > 0; 35 | } 36 | }; 37 | }; 38 | 39 | return Common; 40 | }(Common || {})); 41 | -------------------------------------------------------------------------------- /src/source/vod_frame.js: -------------------------------------------------------------------------------- 1 | var VOD_FRAME = (function() { 2 | this.setup = function(){ 3 | var callback = function(data) { 4 | var srcArray = ['https://redir.atmcdn.pl', 'https://partner.ipla.tv']; 5 | setupDetector(srcArray, data); 6 | }; 7 | MessageReceiver.awaitMessage({ 8 | origin: 'https://vod.pl', 9 | windowReference: window.parent 10 | }, callback); 11 | }; 12 | 13 | var setupDetector = function(srcArray, data){ 14 | var selectors = createArrySelectors(srcArray); 15 | var multiSelector = createMultiSelector(selectors); 16 | var properties = Common.createProperties('div.iplaContainer', multiSelector); 17 | 18 | ElementDetector.detect(properties, function() { 19 | selectors.forEach(function(element){ 20 | if($(element.frameSelector).length > 0){ 21 | MessageReceiver.postUntilConfirmed({ 22 | windowReference: $(element.frameSelector).get(0).contentWindow, 23 | origin: element.src, 24 | message: { 25 | location: data.location 26 | } 27 | }); 28 | } 29 | }); 30 | }); 31 | }; 32 | 33 | var createArrySelectors = function(srcArray){ 34 | return jQuery.map(srcArray, function(src) { 35 | return { 36 | src: src, 37 | frameSelector: 'iframe[src^="' + src + '"]' 38 | } 39 | }); 40 | }; 41 | 42 | var createMultiSelector = function(selectors){ 43 | return $.map(selectors, function(src){ 44 | return src.frameSelector 45 | }).join(', '); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/util/step.js: -------------------------------------------------------------------------------- 1 | var Step = (function(properties){ 2 | var step = { 3 | urlTemplateParts: [], 4 | urlTemplate: '', 5 | /** Will be done before call. It should return an object ready to use by resolveUrl function. **/ 6 | before: function(input){return input}, 7 | /** Will be done after call **/ 8 | after: function (output) {return output}, 9 | /** Processing parameters of url before step */ 10 | resultUrlParams: function (input, template) { 11 | var urlParams = {}; 12 | $.each(input, function (key, value) { 13 | template = template.replace(new RegExp(config.urlParamPattern + key,'g'), value); 14 | urlParams[key] = value; 15 | }); 16 | 17 | return { 18 | url: template, 19 | urlParams: urlParams 20 | }; 21 | }, 22 | /** Processing the url template */ 23 | resolveUrl: function (input, partIndex) { 24 | return this.resultUrlParams(input, this.resolveUrlParts(partIndex)); 25 | }, 26 | /** Is this step remote? */ 27 | isRemote: function(){ 28 | return this.urlTemplate.length > 0; 29 | }, 30 | /** Method of async step */ 31 | method: 'GET', 32 | headers: {}, 33 | responseType: 'json', 34 | retryErrorCodes: [], 35 | /** Method parameters function of async step */ 36 | methodParam: function(){return {}}, 37 | /** Processing url dynamic parts */ 38 | resolveUrlParts: function(partIndex){ 39 | if(this.urlTemplateParts.length){ 40 | return this.urlTemplate.replace(config.urlPartPattern, this.urlTemplateParts[partIndex]); 41 | } 42 | 43 | return this.urlTemplate; 44 | } 45 | }; 46 | 47 | return $.extend(true, step, properties); 48 | }); 49 | -------------------------------------------------------------------------------- /src/source/cda.js: -------------------------------------------------------------------------------- 1 | var CDA = (function() { 2 | var properties = new Configurator({ 3 | observer: { 4 | selector: '.pb-player-content' 5 | }, 6 | injection: { 7 | class: 'right_margin' 8 | }, 9 | chains: { 10 | videos: [ 11 | new Step({ 12 | before: function(input){ 13 | return getDestinationUrl(); 14 | }, 15 | after: function (input) { 16 | return grabVideoData(input); 17 | } 18 | }) 19 | ] 20 | } 21 | }); 22 | 23 | var getDestinationUrl = function(){ 24 | var url = $("video.pb-video-player").attr('src'); 25 | if(url !== undefined){ 26 | /** HTML5 player */ 27 | if(!url.match(/blank\.mp4/)){ 28 | return url; 29 | } 30 | /** Flash player - l is an existing variable on page */ 31 | else if(l !== undefined){ 32 | return l; 33 | } 34 | } 35 | throw new Exception(config.error.id, window.location.href); 36 | }; 37 | 38 | var grabVideoData = function(data){ 39 | var items = []; 40 | var title = $('meta[property="og:title"]'); 41 | var quality = $('.quality-btn-active'); 42 | var videoDesc = quality.length > 0 ? quality.text() : '-'; 43 | items.push(Tool.mapDescription({ 44 | source: 'CDA', 45 | key: videoDesc, 46 | video: videoDesc, 47 | audio: '-', 48 | url: data 49 | })); 50 | return { 51 | title: title.length > 0 ? title.attr('content').trim() : 'brak danych', 52 | cards: {videos: {items: items}} 53 | }; 54 | }; 55 | 56 | this.setup = function(){ 57 | Common.run(properties); 58 | }; 59 | }); 60 | -------------------------------------------------------------------------------- /src/source/wp.js: -------------------------------------------------------------------------------- 1 | var WP = (function() { 2 | var properties = new Configurator({ 3 | observer: { 4 | anchor: 'body', 5 | selector: 'div.npp-container' 6 | }, 7 | chains: { 8 | videos: [ 9 | new Step({ 10 | urlTemplate: 'https://wideo.wp.pl/player/mid,#videoId,embed.json', 11 | before: function (input) { 12 | return idParser(); 13 | }, 14 | after: function (output) { 15 | return grabVideoData(output); 16 | } 17 | }) 18 | ] 19 | } 20 | }); 21 | 22 | var idParser = function () { 23 | try { 24 | var id = window.location.href.match(/^(.*)-(\d+)v$/)[2]; 25 | //__NEXT_DATA__ is a variable on page 26 | return __NEXT_DATA__.props.initialPWPState.material[id].mid; 27 | } 28 | catch(e){ 29 | throw new Exception(config.error.id, window.location.href); 30 | } 31 | }; 32 | 33 | var grabVideoData = function(data){ 34 | var items = []; 35 | var urls = (data.clip || {}).url || {}; 36 | if(urls && urls.length > 0){ 37 | $.each(urls, function( index, value ) { 38 | if(value.type === 'mp4@avc'){ 39 | var videoDesc = value.quality + ', ' + value.resolution; 40 | items.push(Tool.mapDescription({ 41 | source: 'WP', 42 | key: value.quality, 43 | video: videoDesc, 44 | url: value.url 45 | })); 46 | } 47 | }); 48 | return { 49 | title: data.clip.title, 50 | cards: {videos: {items: items}} 51 | } 52 | } 53 | throw new Exception(config.error.noSource, window.location.href); 54 | }; 55 | 56 | this.setup = function(){ 57 | Common.run(properties); 58 | }; 59 | }); 60 | -------------------------------------------------------------------------------- /src/starter.js: -------------------------------------------------------------------------------- 1 | var Starter = (function(Starter) { 2 | var sources = [ 3 | {objectName: 'TVP', urlPattern: new RegExp( 4 | '^https:\/\/(vod|cyfrowa)\.tvp\.pl\/video\/.*$|' + 5 | '^https?:\/\/.*\.tvp\.(pl|info)\/sess\/TVPlayer2\/embed.*$|' + 6 | '^https?:\/\/((?!wiadomosci).)*\.tvp\.pl\/\\d{6,}\/.*$|' + 7 | '^https?:\/\/w{3}\.tvpparlament\.pl\/sess\/.*' 8 | ) 9 | }, 10 | {objectName: 'TVN', urlPattern: new RegExp('^https:\/\/(?:w{3}\.)?(?:tvn)?player\.pl\/')}, 11 | {objectName: 'CDA', urlPattern: new RegExp('^https:\/\/.*\.cda\.pl\/')}, 12 | {objectName: 'VOD', urlPattern: new RegExp('^https:\/\/vod.pl\/')}, 13 | {objectName: 'VOD_IPLA', urlPattern: 14 | new RegExp( 15 | '^https:\/\/partner\.ipla\.tv\/embed\/|' + 16 | '^https:\/\/.*\.redcdn\.pl\/file\/o2\/redefine\/partner\/' 17 | ) 18 | }, 19 | {objectName: 'IPLA', urlPattern: 20 | new RegExp( 21 | '^https:\/\/polsatboxgo\.pl\/|' + 22 | '^https:\/\/polsatgo\.pl\/' 23 | ) 24 | }, 25 | {objectName: 'WP', urlPattern: new RegExp('^https:\/\/wideo\.wp\.pl\/')}, 26 | {objectName: 'NINATEKA', urlPattern: new RegExp('^https:\/\/ninateka.pl\/')}, 27 | {objectName: 'ARTE', urlPattern: new RegExp('^https:\/\/w{3}\.arte\.tv\/.*\/videos\/')}, 28 | {objectName: 'VOD_FRAME', urlPattern: new RegExp('^https:\/\/pulsembed\.eu\/')}, 29 | {objectName: 'TV_TRWAM', urlPattern: new RegExp('^https:\/\/tv-trwam\.pl\/local-vods\/')} 30 | ]; 31 | 32 | Starter.start = function() { 33 | sources.some(function(source){ 34 | if(source.urlPattern.exec(location.href)){ 35 | console.info('voddownloader: context: ' + source.objectName + ', url: ' + location.href); 36 | var object = eval('new ' + source.objectName + '()'); 37 | object.setup(); 38 | return true; 39 | } 40 | }); 41 | }; 42 | 43 | return Starter; 44 | }(Starter || {})); 45 | -------------------------------------------------------------------------------- /src/util/notification.js: -------------------------------------------------------------------------------- 1 | var Notification = (function(Notification) { 2 | var create = function(title, bodyContent, special) { 3 | var specialContentClasses = special ? ' special-color white-text' : ''; 4 | var content = $('
').addClass('toast notification' + specialContentClasses).attr('role', 'alert') 5 | .attr('aria-live', 'assertive').attr('aria-atomic', 'true') 6 | .attr('name', special ? 'special' : 'normal').attr('data-delay', '5000'); 7 | var header = $('
').addClass('toast-header special-color-dark white-text'); 8 | var warnIcon = $('').addClass('fas fa-exclamation-triangle pr-2'); 9 | var notificationTitle = $('').addClass('mr-auto').text(title); 10 | var time = $('').text(new Date().toLocaleTimeString()); 11 | var close = $('