aboutsummaryrefslogtreecommitdiff
/*
  Copyright (C) 2010 - 2017 Sebastian Luncan
  Copyright (C) 2017 Arun Isaac <arunisaac@systemreboot.net>

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program. If not, see <http://www.gnu.org/licenses/>.

  Website: http://isebaro.com/viewtube
  Contact: http://isebaro.com/contact
*/

// ==========Variables========== //

(function() {
    // Userscript
    var userscript = 'ViewTube';

    // Page
    var page = {dom: document.documentElement.outerHTML, win: window, url: window.location.href};

    // Player
    var player = {};
    var mimetypes = {
	'MPEG': 'video/mpeg',
	'MP4': 'video/mp4',
	'WebM': 'video/webm',
	'FLV': 'video/x-flv',
	'MOV': 'video/quicktime',
	'M4V': 'video/x-m4v',
	'AVI': 'video/x-msvideo',
	'3GP': 'video/3gpp',
    };

    // Links
    var website = 'https://git.systemreboot.net/youtube-noscript-shim/about';
    var contact = 'mailto:arunisaac@systemreboot.net';

    // ==========Functions========== //

    function createElement (type, attributes, parent) {
	var element = document.createElement(type);
	for (var key in attributes)
	    element.setAttribute(key, attributes[key]);
	if (parent) parent.appendChild(element);
	return element;
    }

    function createVideoElement (attributes, parent, ...children) {
	return createElement.apply(null, ["video", Object.assign({controls: "controls", autoplay: "autoplay", volume: 0.8}, attributes), parent].concat(children));
    }

    function playDASHwithHTML5() {
	function playAudio (play) {
	    if (play) player["contentAudio"].play();
	    else player["contentAudio"].pause();
	}

	if (player['videoPlay'].indexOf('MP4') != -1) {
	    player["contentVideo"] = createVideoElement({src: player["videoList"][player["videoPlay"].replace(/MP4/, "Video MP4")]});
	    if (player['videoList']['High Bitrate Audio Opus'])
		player["contentAudio"] = createVideoElement({src: player["videoList"]["High Bitrate Audio Opus"]});
	    else if (player['videoList']['Medium Bitrate Audio Opus'])
		player['contentAudio'] = createVideoElement({src: player['videoList']['Medium Bitrate Audio Opus']});
	    else player['contentAudio'] = createVideoElement({src: player['videoList']['Medium Bitrate Audio MP4']});
	}
	else {
	    player["contentVideo"] = createVideoElement({src: player["videoList"][player["videoPlay"].replace(/WebM/, "Video WebM")]});
	    if (player['videoList']['High Bitrate Audio Opus'])
		player["contentAudio"] = createVideoElement({src: player["videoList"]["High Bitrate Audio Opus"]});
	    else if (player['videoList']['Medium Bitrate Audio Opus'])
		player["contentAudio"] = createVideoElement({src: player["videoList"]["Medium Bitrate Audio Opus"]});
	    else player["contentAudio"] = createVideoElement({src: player["videoList"]["Medium Bitrate Audio WebM"]});
	}
	player['contentAudio'].pause();
	player['contentVideo'].addEventListener('play', playAudio.bind(null, true), false);
	player['contentVideo'].addEventListener('pause', playAudio.bind(null, false), false);
	player['contentVideo'].addEventListener('ended', function() {
	    player['contentVideo'].pause();
	    player['contentAudio'].pause();
	}, false);
	player['contentVideo'].addEventListener('timeupdate', function() {
	    if (player['contentAudio'].paused && !player['contentVideo'].paused)
		player['contentAudio'].play();
	    if (Math.abs(player['contentVideo'].currentTime - player['contentAudio'].currentTime) >= 0.30)
		player['contentAudio'].currentTime = player['contentVideo'].currentTime;
	}, false);
	player["contentAudio"].classList.add("hide");
	player['contentVideo'].appendChild(player['contentAudio']);
    }

    function playMyVideo() {
	if (player['videoList'][player['videoPlay']] == 'DASH')
	    playDASHwithHTML5();
	else player["contentVideo"] = createVideoElement({src: player["videoList"][player["videoPlay"]],
							  poster: player["videoThumb"]});
	player['playerWindow'].appendChild(player['contentVideo']);
    }

    function cleanMyContent(content, unesc) {
	var myNewContent = content;
	if (unesc) myNewContent = unescape(myNewContent);
	return myNewContent.replace(/\\u0025/g,'%').replace(/\\u0026/g,'&').replace(/\\/g,'').replace(/\n/g,'');
    }

    function getMyContent(url, pattern, clean) {
	var myPageContent, myVideosParse;
	// Get content
	if (url == page.url) myPageContent = page.dom;
	else {
	    var xmlHTTP = new XMLHttpRequest();
	    xmlHTTP.open('GET', url, false);
	    xmlHTTP.send();
	    myPageContent = xmlHTTP.responseText;
	}
	// Match pattern
	if (pattern == "TEXT") return myPageContent;
	else {
	    if (clean) myPageContent = cleanMyContent(myPageContent, true);
	    myVideosParse = myPageContent.match(pattern);
	    return myVideosParse ? myVideosParse[1] : null;
	}
    }

    // =====YouTube===== //

    // Add stylesheet
    createElement("link", {rel: "stylesheet",
			   type: "text/css",
			   href: browser.extension.getURL("viewtube.css")},
		  document.head);

    /* Video Availability */
    var ytVideoUnavailable = document.querySelector("#player-unavailable");
    if (ytVideoUnavailable && (ytVideoUnavailable.className.indexOf('hid') == -1)) {
	var ytAgeGateContent = document.querySelector("#watch7-player-age-gate-content");
	if ((!ytAgeGateContent) || (ytAgeGateContent.indexOf('feature=private_video') != -1)) return;
    }

    /* Get Player Window */
    var ytPlayerWindow = document.querySelector("#player");
    if (!ytPlayerWindow) console.log("Couldn't get the player element.");
    else {
	/* Get Video Thumbnail */
	var ytVideoThumb = getMyContent(page.url, 'link\\s+itemprop="thumbnailUrl"\\s+href="(.*?)"', false)
	    || getMyContent(page.url, 'meta\\s+property="og:image"\\s+content="(.*?)"', false)
	    || ('https://img.youtube.com/vi/' + page.url.match(/(\?|&)v=(.*?)(&|$)/)[2] + '/0.jpg');

	/* Get Videos Content */
	var ytVideosEncodedFmts, ytVideosAdaptiveFmts, ytVideosContent, ytHLSVideos, ytHLSContent, ytVideoID, ytVideosInfo;
	ytVideosAdaptiveFmts = getMyContent(page.url, '"adaptive_fmts":\\s*"(.*?)"', false)
	    || getMyContent(page.url, '\\\\"adaptive_fmts\\\\":\\s*\\\\"(.*?)\\\\"', false);
	if (ytVideosEncodedFmts = getMyContent(page.url, '"url_encoded_fmt_stream_map":\\s*"(.*?)"', false)
	    || getMyContent(page.url, '\\\\"url_encoded_fmt_stream_map\\\\":\\s*\\\\"(.*?)\\\\"', false))
	    ytVideosContent = ytVideosEncodedFmts;
	else if (ytHLSVideos = getMyContent(page.url, '"hlsvp":\\s*"(.*?)"', false)
		 || getMyContent(page.url, '\\\\"hlsvp\\\\":\\s*\\\\"(.*?)\\\\"', false)) {
	    ytHLSVideos = cleanMyContent(ytHLSVideos, false);
	    if (ytHLSVideos.indexOf('keepalive/yes/') != -1)
		ytHLSVideos = ytHLSVideos.replace('keepalive/yes/', '');
	}
	else if (ytVideoID = page.url.match(/(\?|&)v=(.*?)(&|$)/)[2]) {
	    var ytVideoSts = getMyContent(page.url.replace(/watch.*?v=/, 'embed/').replace(/&.*$/, ''), '"sts"\\s*:\\s*(\\d+)', false);
	    var ytVideosInfoURL = page.win.location.protocol + '//' + page.win.location.hostname + '/get_video_info?video_id=' + ytVideoID + '&eurl=https://youtube.googleapis.com/v/' + ytVideoID + '&sts=' + ytVideoSts;
	    if ((ytVideosInfo = getMyContent(ytVideosInfoURL, 'TEXT', false)) &&
		(ytVideosEncodedFmts = ytVideosInfo.match(/url_encoded_fmt_stream_map=(.*?)&/)[1]))
		ytVideosContent = cleanMyContent(ytVideosEncodedFmts, true);
	    if (!ytVideosAdaptiveFmts && (ytVideosAdaptiveFmts = ytVideosInfo.match(/adaptive_fmts=(.*?)&/)[1]))
		ytVideosAdaptiveFmts = cleanMyContent(ytVideosAdaptiveFmts, true);
	}

	if (ytVideosAdaptiveFmts && !ytHLSVideos) {
	    if (ytVideosContent) ytVideosContent += ',' + ytVideosAdaptiveFmts;
	    else ytVideosContent = ytVideosAdaptiveFmts;
	}

	/* Playlist */
	var ytPlaylist, ytPlaceholderPlaylist;
	if ((ytPlaylist = document.querySelector("#player-playlist"))
	    && (ytPlaceholderPlaylist = document.querySelector("#placeholder-playlist")))
	    ytPlaceholderPlaylist.appendChild(ytPlaylist);

	/* Create Player */
	var ytDefaultVideo = 'Low Definition MP4';
	function ytPlayer() {
	    player = {
		'playerSocket': ytPlayerWindow,
		'playerWindow': document.querySelector("#player-api"),
		'videoList': ytVideoList,
		'videoPlay': ytDefaultVideo,
		'videoThumb': ytVideoThumb
	    };
	    playMyVideo();
	}

	/* Parse Videos */
	function ytVideos() {
	    var ytVideoFormats = {
		'5': 'Very Low Definition FLV',
		'17': 'Very Low Definition 3GP',
		'18': 'Low Definition MP4',
		'22': 'High Definition MP4',
		'34': 'Low Definition FLV',
		'35': 'Standard Definition FLV',
		'36': 'Low Definition 3GP',
		'37': 'Full High Definition MP4',
		'38': 'Ultra High Definition MP4',
		'43': 'Low Definition WebM',
		'44': 'Standard Definition WebM',
		'45': 'High Definition WebM',
		'46': 'Full High Definition WebM',
		'82': 'Low Definition 3D MP4',
		'83': 'Standard Definition 3D MP4',
		'84': 'High Definition 3D MP4',
		'85': 'Full High Definition 3D MP4',
		'100': 'Low Definition 3D WebM',
		'101': 'Standard Definition 3D WebM',
		'102': 'High Definition 3D WebM',
		'135': 'Standard Definition Video MP4',
		'136': 'High Definition Video MP4',
		'137': 'Full High Definition Video MP4',
		'138': 'Ultra High Definition Video MP4',
		'139': 'Low Bitrate Audio MP4',
		'140': 'Medium Bitrate Audio MP4',
		'141': 'High Bitrate Audio MP4',
		'171': 'Medium Bitrate Audio WebM',
		'172': 'High Bitrate Audio WebM',
		'244': 'Standard Definition Video WebM',
		'247': 'High Definition Video WebM',
		'248': 'Full High Definition Video WebM',
		'249': 'Low Bitrate Audio Opus',
		'250': 'Medium Bitrate Audio Opus',
		'251': 'High Bitrate Audio Opus',
		'266': 'Ultra High Definition Video MP4',
		'272': 'Ultra High Definition Video WebM',
		'298': 'High Definition Video MP4',
		'299': 'Full High Definition Video MP4',
		'302': 'High Definition Video WebM',
		'303': 'Full High Definition Video WebM',
		'313': 'Ultra High Definition Video WebM'
	    };
	    var ytVideoParse, ytVideoCode, myVideoCode;
	    for (var ytVideo of ytVideosContent.split(',')) {
		if ((!ytVideo.match(/^url/)) && (ytVideoParse = ytVideo.match(/(.*)(url=.*$)/)))
		    ytVideo = ytVideoParse[2] + '&' + ytVideoParse[1];
		if ((ytVideoCode = (ytVideo.match(/itag=(\d{1,3})/) || [])[1])
		    && (myVideoCode = ytVideoFormats[ytVideoCode])) {
		    ytVideo = cleanMyContent(ytVideo, true);
		    ytVideo = ytVideo.replace(/url=/, "");
		    if ((ytVideo.match(/itag=/g) || []).length > 1)
			ytVideo = ytVideo.replace(/itag=\d{1,3}/, "");
		    if ((ytVideo.match(/clen=/g) || []).length > 1)
			ytVideo = ytVideo.replace(/clen=\d+/, "");
		    if ((ytVideo.match(/lmt=/g) || []).length > 1)
			ytVideo = ytVideo.replace(/lmt=\d+/, "");
		    ytVideo = ytVideo.replace(/type=(video|audio).*?/, "").replace(/xtags=[^%=]*?/, "");
		    ytVideo = ytVideo.replace(/&&/g, "&").replace(/^&/, "").replace(/&$/, "");
		    if (ytVideo.match(/&sig=/)) ytVideo = ytVideo.replace(/&sig=/, '&signature=');
		    else if (ytVideo.match(/&s=/)) {
			console.log("Decrypting cipher signatures not supported.");
			ytVideo = "";
		    }
		    ytVideo = cleanMyContent(ytVideo, true);
		    if (ytVideo.indexOf('ratebypass') == -1) ytVideo += '&ratebypass=yes';
		    if (ytVideo && ytVideo.indexOf('http') == 0)
			ytVideoList[myVideoCode] = ytVideo;
		}
	    }

	    if (Object.keys(ytVideoList).length > 0) {
		for (var container of ["Standard", "High", "Full High", "Ultra High"])
		    for (var definition of ["MP4", "WebM"])
			if (!ytVideoList[definition + " Definition " + container] &&
			    ytVideoList[definition + " Definition Video " + container])
			    ytVideoList[definition + " Definition " + container] = "DASH";
		ytPlayer();
	    }
	    else if (ytVideosContent.indexOf("conn=rtmp") != -1)
		console.log("This video uses the RTMP protocol and is not supported.");
	    else console.log("Couldn't get any video.");
	}

	/* Parse HLS */
	function ytHLS() {
	    var ytHLSFormats = {
		'92': 'Very Low Definition MP4',
		'93': 'Low Definition MP4',
		'94': 'Standard Definition MP4',
		'95': 'High Definition MP4',
		'96': 'Full High Definition MP4'
	    };
	    ytVideoList["Any Definition MP4"] = ytHLSVideos;
	    if (ytHLSContent) {
		var ytHLSVideo, ytVideoCodeParse, myVideoCode;
		if (ytHLSVideos = ytHLSContent.match(/(http.*?m3u8)/g))
		    for (var ytHLSVideo in ytHLSVideos)
			if (ytVideoCode = (ytHLSVideo.match(/\/itag\/(\d{1,3})\//) || [])[1]) {
			    myVideoCode = ytHLSFormats[ytVideoCode];
			    if (myVideoCode && ytHLSVideo)
				ytVideoList[myVideoCode] = ytHLSVideo;
			}
	    }
	    ytDefaultVideo = 'Any Definition MP4';
	    ytPlayer();
	}

	/* Get Videos */
	var ytVideoList = {}, ytScriptURL;
	if (ytVideosContent) {
	    if (ytVideosContent.match(/&s=/) || ytVideosContent.match(/,s=/) || ytVideosContent.match(/u0026s=/)) {
		if (ytScriptURL = getMyContent(page.url, '"js":\\s*"(.*?)"', true)
		    || getMyContent(page.url.replace(/watch.*?v=/, 'embed/').replace(/&.*$/, ''), '"js":\\s*"(.*?)"', true)) {
		    ytScriptURL = page.win.location.protocol + ytScriptURL;
		    try {
			if (ytScriptSrc = getMyContent(ytScriptURL, 'TEXT', false)) ytDecryptFunction();
			ytVideos();
		    }
		    catch(e) {
			try {
			    var oReq = new XMLHttpRequest();
			    oReq.open("GET", ytScriptURL);
			    oReq.onload = function(response) {
				if (response.readyState === 4 && response.status === 200 && response.responseText) {
				    ytScriptSrc = response.responseText;
				    ytDecryptFunction();
				    ytVideos();
				}
				else console.log("Couldn't get the signature content.");
			    };
			    oReq.onerror = console.log.bind(null, "Couldn't make the request. Make sure your browser user scripts extension supports cross-domain requests.");
			    oReq.send();
			}
			catch(e) {
			    console.log("Couldn't make the request. Make sure your browser user scripts extension supports cross-domain requests.");
			}
		    }
		}
		else console.log("Couldn't get the signature link.");
	    }
	    else ytVideos();
	}
	else if (ytHLSVideos) {
	    try {
		ytHLSContent = getMyContent(ytHLSVideos, 'TEXT', false);
		ytHLS();
	    }
	    catch(e) {
		try {
		    var oReq = new XMLHttpRequest();
		    oReq.open("GET", ytHLSVideos);
		    oReq.onload = function(response) {
			if (response.readyState === 4 && response.status === 200 && response.responseText)
			    ytHLSContent = response.responseText;
			ytHLS();
		    };
		    oReq.onerror = ytHLS;
		    oReq.send();
		}
		catch(e) {
		    ytHLS();
		}
	    }
	}
	else console.log("Couldn't get the videos content.");
    }

})();