Download FOX+CN+CC Videos as MP4

Скачать видео каналов FOX, Cartoon Network, Comedy Central одним кликом. Прямые ссылки на видео под плеером (fox-fan.ru, cn-fan.ru, cc-fan.tv)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        Download FOX+CN+CC Videos as MP4
// @description Скачать видео каналов FOX, Cartoon Network, Comedy Central одним кликом. Прямые ссылки на видео под плеером (fox-fan.ru, cn-fan.ru, cc-fan.tv)
// @namespace   https://greasyfork.org/users/136230
// @include     *://*.fox-fan.ru/series.php*
// @include     *://*.fox-fan.tv/series.php*
// @include     *://*.cn-fan.ru/series.php*
// @include     *://*.cn-fan.tv/series.php*
// @include     *://*.cc-fan.tv/series.php*
// @include     *://*.cc-fan.ru/series.php*
// @include     *://*.fox-fan.ru/video*
// @include     *://88.150.190.2/video*
// @include     *://81.94.196.236/video*
// @include     *://109.169.87.137/video*
// @connect     fox-fan.tv
// @connect     fox-fan.ru
// @connect     cn-fan.tv
// @connect     cn-fan.ru
// @connect     cc-fan.tv
// @connect     88.150.190.2
// @connect     81.94.196.236
// @connect     109.169.87.137
// @version     1.3.1
// @run-at      document-start
// @grant       GM_xmlhttpRequest
// @grant       GM.xmlHttpRequest
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// ==/UserScript==

var RANDOM = 1687511;//Math.round(Math.random()*1.e6 + 1.e6);
(function(){
	var clog = console.log;
    var { hostname, pathname } = window.location;
    clog('fox-cn-cc-downloader', self === top ? 'top' : 'child', hostname + pathname);
    function DOMReady(callback){
        var handler = {};
        var promise = new Promise(function(resolve){
            handler.resolve = resolve;
        });
        var fn = function(){
            handler.resolve();
            if (typeof callback === 'function') {
                callback();
            }
        };
        switch (document.readyState) {
            case 'loading':
                document.addEventListener('DOMContentLoaded', function(e){ fn(e); });
                break;
            default:
                setTimeout(fn, 10);
        }
        return promise;
    }
	var userOptions = initOptions();
	userOptions.set({
		'delim': ' - ',
		'space': ' ', 
		'addEpisodeEng': true,// добавить англ. название эпизода
		'delay': 500,
	});
	var isSource = downloadFromSource();
	if( isSource )
		return;
	else
		document.addEventListener('DOMContentLoaded', start, false);
	function downloadFromSource()
	{
		var hash = window.location.hash,
			json, name, source;
		if( hash )
		{
			json = hash.slice(1);
			json = decodeURIComponent(json);
            try {
                json = json ? JSON.parse(json) : {};
            } catch (e) {
                json = {};
            }
            clog('download data: ', json);
            source = json.source;
            name = json.name;
			// source = window.location.origin + window.location.pathname;
			//alert("name: " + name + "\nsource: " + source);
		}else
			return false;
		if( source && name && /\.mp4$/.test(name) )
		{
			var sendMessage = function(){
                clog('send message', 'close-iframe');
                window.parent.postMessage({msg: 'close-iframe', 'id': window.self.name,}, '*');
            }, closeWindow = function(){
                clog('close window', window.location.href);
                window.close();
            }, videoClose = function(){
                clog('close video');
				remove(document.querySelector('video'));
				if( window.self !== window.parent )
					setTimeout( sendMessage, userOptions.val('delay') );
				else
					setTimeout( closeWindow, userOptions.val('delay') );
			};
            clog('dom.readyState: ', document.readyState);
            DOMReady(function(){
				fileDownloader(name, source, videoClose);
				var video = document.querySelector('video');
				if( video ) video.addEventListener('error', videoClose, false);
            });
			// document.addEventListener('readystatechange', function(event){
                // clog('document.readyState: ', this.readyState);
				// if( this.readyState === 'interactive' )
				// {
					// fileDownloader(name, source);
					// var video = document.querySelector('video');
					// if( video ) video.addEventListener('error', videoClose, false);
				// }
				// else if( this.readyState === 'complete' )
					// videoClose();
			// }, false);
			return true;
		}
		return false;
	}
	function fileDownloader( name, resource, callback )
	{
		var a = document.createElement('a'),
			body = document.body || document.getElementsByTagName('body')[0];
		a.href = resource;
		a.setAttribute('download', name);
		body.appendChild(a);
		a.click();
        setTimeout(function(){
            body.removeChild(a);
            if (typeof callback === 'function') {
                callback();
            }
        }, 10);
	}
	function start()
	{
		console.log("start Download FOX+CN+CC Videos as MP4..");
		getLinks();
		showDetail('description');// описание
		//showAll();
	}
	function getLinks()
	{
		var links = document.querySelectorAll('div#voice a');
		for( var i = 0, href; i < links.length; ++i)
		{
			href = links[i].href;
            clog('makeRequest', href);
			GM.xmlHttpRequest({
				url: href,
				method: 'GET',
				onload: handleXHR,
			});
		}
	}
	function handleXHR( xhr )
	{
		var doc = document.implementation.createHTMLDocument("");
		doc.documentElement.innerHTML = xhr.responseText;
		setLink(doc, xhr.finalUrl);
	}
	function setLink(doc, url)
	{
		doc = doc || document;
		var script = doc.querySelector('#centerSeries > script'),
			video_obj, video_src, subt, div, video_div, video_link, file_name, player;
		if( !script )
		{
			player = doc.querySelector('div[id*="player"]');
			if( player )
				script = player.nextElementSibling;
		}
        video_obj = script ? getVideoObject( script.innerHTML ) : null;
        if( video_obj )
        {
            video_src = video_obj.file;
            subt = video_obj.subtitle;
        }
        if( !video_src )
        {
            player = doc.querySelector('div[id*="player"]');
            video_div = player ? player.querySelector('video') : null;
            video_src = video_div ? video_div.src : null;
        }
        if( !video_src )
        {
            var _scripts = doc.querySelectorAll('script');
            _scripts = [].filter.call(_scripts, function(s){ return !s.src; });
            _scripts.forEach(function(s, i){
                var html = s.innerHTML || '';
                var idx = html.indexOf('player.api(');
                if (idx === -1) {
                    return;
                }
                html = html.split('\n').join(' ');
                var match = html.match(/player\.api\([^\)]+\)/g);
                if (!match) {
                    return;
                }
                match = [].slice.call(match);
                match = match.map(function(s){
                    s = s.replace(/^(player\.api\()([^\)]+)(\))$/, function(m, p1, p2, p3){
                        return p2.replace(/'/g, '"');
                    });
                    return JSON.parse('[' + s + ']');
                });
                match = match.reduce(function(acc, cur){
                    if (cur.indexOf('play') !== -1) {
                        if (!acc.play) {
                            acc.play = cur;
                        } else if (Array.isArray(acc.play)) {
                            acc.play.push.apply(acc.play, cur);
                        } else {
                            acc.play = [acc.play, cur];
                        }
                    }
                    if (cur.indexOf('subtitle') !== -1) {
                        if (!acc.subtitle) {
                            acc.subtitle = cur;
                        } else if (Array.isArray(acc.subtitle)) {
                            acc.subtitle.push.apply(acc.subtitle, cur);
                        } else {
                            acc.subtitle = [acc.subtitle, cur];
                        }
                    }
                    return acc;
                }, {});
                if (match.play) {
                    video_src = match.play.find(function(item){
                        return /^((https?)?\:)?\/\/([^\/]+)([^?]+)?([^#]+)?(.*)$/.test(item);
                    });
                }
                if (match.subtitle) {
                    subt = match.subtitle.find(function(item){
                        return /^((https?)?\:)?\/\/([^\/]+)([^?]+)?([^#]+)?(.*)$/.test(item);
                    });
                }
            });
        }
        clog('video_src: ', video_src);
        clog('subtitles: ', subt);
		if( video_src )
		{
			div = document.getElementById('video-links-' + RANDOM) || createVideoDiv('video-links-' + RANDOM);
			video_div = document.createElement('div');
			file_name = getFileName(doc);
			clog("file name: " + file_name);
			video_div.innerHTML = '<a href="' + video_src + '" title="Скачать" ' +
				'data-file-name="' + file_name + '" data-file-ext="mp4" data-file-source="' + video_src + '" ' +
				(subt ? ('data-subtitle="' + subt + '"') : '') + ' ' +
				'class="video-link">' + file_name + '</a>';
			div.appendChild( video_div );
			video_link = video_div.querySelector('a');
			video_link.addEventListener('click', handleDownloadEvent, false);
            clog('makeRequest HEAD ', video_src);
			GM.xmlHttpRequest({
				url: video_src,
				method: 'HEAD',
				context: {url: video_src},
				onload: function(xhr){
					if( xhr.status != 200 )
					{
						console.error("Error: xhr.status = ", xhr.status, xhr.statusText);
						return;
					}
					var videoSize = getContentLength(xhr.responseHeaders),
						smartSize = "" + (videoSize/(1024*1024)).toFixed(1) + " Mb";
					video_link.setAttribute('title', "Скачать, " + smartSize );
					console.log("source: ", xhr.context.url, "size: ", smartSize);
				},
			});
		}else{
			console.error("can't find video source");
		}
	}
	function getContentLength(headersStr)
	{
		var headers = headersStr.split('\r\n');
		for( var i = 0, h; i < headers.length; ++i )
		{
			h = headers[i];
			if( h.indexOf('Content-Length') != -1 )
				return parseInt(h.match(/\d+/)[0], 10);
		}
		return 0;
	}
	function createVideoDiv( id )
	{
		var div = document.createElement('div');
		div.setAttribute('id', id);
		var html = '' +
		'<font class="size18 link_20" title="Нажмите на ссылку, чтобы скачать">Ссылки на видео</font>' +
		'<br><div id="tochki2" class="tocka"></div>';
		div.innerHTML = html;
		var insPlace = document.getElementById('4');
		return insPlace.parentNode.insertBefore( div, insPlace.nextSibling );
	}
	function getVideoObject( text )
	{
		if( !text )
			return null;
		var str = text.match(/{.*}/)[0];
		str = str.replace(/([\w]+)\:([^\/]{1})/g, function(m, p1, p2){
			return '"' + p1 + '":' + p2;
		});
		str = str.replace(/'/g, '"');
		return JSON.parse( str );
	}
	function getFileName( doc )
	{
		this.fn_count = this.fn_count || 0;
		++this.fn_count;
		var delim = userOptions.val('delim'),
			space = userOptions.val('space');
		doc = doc || document;
		var topContent = doc.querySelector('.topContent'),
			topContentDiv, topContentLinks;
		if( topContent )
		{
			topContentDiv = doc.querySelector('.topContent > div');
			topContentLinks = doc.querySelectorAll('.topContent > a');
		}else{
			topContentDiv = doc.querySelector('table.content tr > td > div');
			topContentLinks = doc.querySelectorAll('table.content tr > td > a');
		}
		var voiceId, videoId, videoIdStr,
			seasonNum, episodeNum;
		if( topContentDiv )
		{
			// ID озвучки и ID видео
			voiceId = topContentDiv.querySelector('#voice').innerHTML;
			videoId = topContentDiv.querySelector('#actual').innerHTML;
		}
		if( videoId )
		{
			var videoIdStr = videoId.toString(),
				videoIdMatch = videoIdStr.match(/(\d+)(\D)?/);
			// номер сезон и номер эпизода
			seasonNum = videoIdMatch[1].slice(0, -2);
			episodeNum = videoIdMatch[1].slice(-2) + (videoIdMatch[2] || '');
		}
		var seriesRus, episodeRus, episodeEng, voiceRus;
		if( topContentLinks )
		{
			// название серий и название эпизода (рус.)
			seriesRus = topContentLinks[1].innerText;
			var lastLink = last(topContentLinks),
				regExp = new RegExp('id=' + videoId);
			if( videoId && lastLink && regExp.test(getLocation(lastLink.href, 'search')) )
				episodeRus = lastLink.innerText;
		}
		var titleSeriesEng = doc.querySelector('#titleSeriesEng > span'),
			titleSeries = doc.querySelector('#titleSeries');
		// название эпизода (англ.)
		if( titleSeriesEng )
			episodeEng = titleSeriesEng.innerText;
		if( !episodeRus && titleSeries )
			episodeRus = titleSeries.innerText;
		var voice = doc.querySelectorAll('#voice .voiceOn a');
		if( voice )
		{
			// название озвучки (рус.)
			if( voice.length == 1 )
				voiceRus = voice[0].innerText;
			else if( voiceId !== undefined )
			{
				for( var i = 0, regExp = new RegExp('voice=' + voiceId + '\\D?'); i < voice.length; ++i )
				{
					if( regExp.test(getLocation(voice[i].href, 'search')) )
						voiceRus = voice[i].innerText;
				}
			}
		}
		var fileName = '';
		fileName += seriesRus.replace(/\s+/g, space) + delim;
		if( seasonNum )
			fileName += 'сезон' + space + padRight(seasonNum, 2, '0') + delim;
		if( episodeNum )
			fileName += 'серия' + space + padRight(episodeNum, 2, '0') + delim;
		if( episodeRus )
			fileName += episodeRus.replace(/\s+/g, space) + delim;
		else if( videoId )
			fileName += 'видео' + space + videoId + delim;
		else
			fileName += 'видео' + space + padRight(this.fn_count, 4, '0') + delim;
		if( episodeEng && userOptions.val('addEpisodeEng') )
			fileName += episodeEng.replace(/\s+/g, space) + delim;
		if( voiceRus )
			fileName += voiceRus.replace(/\s+/g, space);
		else
			fileName = fileName.slice(0, -delim.length);
		return fileName;
	}
	function handleDownloadEvent(event)
	{
		if( event.ctrlKey )
			return;
		event.preventDefault();
		var fileName = this.getAttribute('data-file-name') + '.' + this.getAttribute('data-file-ext');
		downloadFile( fileName, this.getAttribute('data-file-source') );
		var subtitle = this.getAttribute('data-subtitle'),
			subtitleExt = subtitle ? subtitle.match(/\.([^\.]+)$/)[1] : null;
		if( subtitle )
			GM_fileDownloader( this.getAttribute('data-file-name') + '.' + subtitleExt, subtitle );
	}
	function downloadFile( name, source )
	{
        var json = JSON.stringify({ source, name });
        var orig = getLocation(source, 'origin');
		if( orig === window.location.origin )
			fileDownloader( name, source );
		else{
            var hash = '#' + encodeURIComponent(json);
			clog("name: ", name);
			window.open( orig + '/videos' + hash );
			// makeIFrame( orig + '/videos' + hash );
		}
	}
	function makeIFrame( resource )
	{
		this.num = this.num || 0;
		++this.num;
		var iframe = document.createElement('iframe'),
			body = document.body || document.getElementsByTagName('body')[0];
		iframe.src = resource;
		iframe.id = 'iframe-' + this.num;
		iframe.setAttribute('name', 'iframe-' + this.num);
		iframe.setAttribute('style', 'display: none;');
		body.appendChild(iframe);
	}
	function GM_fileDownloader( name, source )
	{
		GM.xmlHttpRequest({
			url: source,
			method: 'GET',
			responseType: 'blob',
			onload: function(xhr){
				if( xhr.status !== 200 )
				{
					console.error("ERROR: status = ", xhr.status, xhr.statusText);
					console.error("url: ", this.url);
					return;
				}
				var wURL = window.webkitURL || window.URL,
					resource = wURL.createObjectURL(xhr.response);
				fileDownloader( name, resource );
				wURL.revokeObjectURL( resource );
			},
		});
	}
	function initOptions()
	{
		function _setDef( v ){this.val = this.def;}
		var retVal = {
			data: {
				'delim': {
					val: null,
					def: ' - ',
					setDef: _setDef,
				},
				'space': {
					val: null,
					def: ' ',
					setDef: _setDef,
				},
				'addEpisodeEng': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				'delay': {
					val: null,
					def: 500,
					setDef: _setDef,
				},
			},
			setDefs: function(){
				for( var key in this.data )
					this.data[key].setDef();
			},
			val: function( prop, v ){
				if( this.data[prop] )
				{
					if( v !== undefined )
						this.data[prop].val = v;
					return this.data[prop].val;
				}
				return null;
			},
			set: function( obj ){
				for( var key in obj )
					this.val( key, obj[key] );
			},
		};
		retVal.setDefs();
		return retVal;
	}
	function getIDs( n )
	{
		this.ids = this.ids || ['zero1', 'zero2', 'description', 'trivia', 'parody', 'criticism', 'awards', 'censorship'];
		if( n !== undefined )
			return this.ids[n];
		return this.ids;
	}
	function showDetail( id )
	{
		var ids, n, elm;
		if( typeof id === 'number' ){
			elm = document.getElementById(id);
		}else{
			ids = getIDs();
			n = ids.indexOf(id);
			elm = document.getElementById(n);
		}
		if( elm )
			elm.style.display = 'block';
	}
	function showAll()
	{
		for( var i = 1, len = getIDs().length; i < len; ++i )
			showDetail( i );
	}
	function padRight( t, n, fill )
	{
		if( !t || !n )
			return t;
		fill = (fill === undefined ? ' ' : fill );
		t = t.toString();
		while( t.length < n )
			t = fill + t;
		return t;
	}
	function getLocation( url, prop )
	{
		var a = document.createElement('a');
		a.href = url;
		return a[prop];
	}
	function last( arr )
	{
		if( arr && arr.length > 0 )
			return arr[arr.length-1];
		return null;
	}
	function indexOfAttr( arr, attr, val )
	{
		if( !arr || !attr || !val )
			return -1;
		for( var i = 0, len = arr.length, elm; i < len; ++i )
		{
			elm = arr[i];
			if( elm[attr] == val )
				return i;
		}
		return -1;
	}
	function remove( elm )
	{
		if( elm && elm.parentNode )
			return elm.parentNode.removeChild(elm);
		return elm;
	}
	window.addEventListener('message', recieveMessage, false);
	function recieveMessage(event)
	{
		if( event.data && event.data.msg === ('close-iframe') && event.data.id )
		{
			var iframe = document.querySelector('iframe[name="' + event.data.id + '"]');
			clog("close iframe: ", iframe);
			setTimeout(function(){
				remove(iframe);
			}, userOptions.val('delay') );
		}
	}
})();