Greasy Fork is available in English.

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') );
		}
	}
})();