avgleHPD - avgle HLS playlist downloader

Decrypt and download HLS playlist(m3u8) of avgle.com video in browser.

// ==UserScript==
// @name         avgleHPD - avgle HLS playlist downloader
// @namespace    https://github.com/avotoko/avgle-HLS-playlist-downloader
// @version      0.1.2
// @icon         https://avgle.com/favicon.ico
// @description  Decrypt and download HLS playlist(m3u8) of avgle.com video in browser.
// @author       avotoko
// @homepage     https://avotoko.blogspot.com/2020/04/avgle-hls-playlist-downloader.html
// @supportURL   https://github.com/avotoko/avgle-HLS-playlist-downloader
// @match        *://avgle.com/video/*
// @connect           *
// @run-at            document-idle
// @grant             unsafeWindow
// @grant             GM_addStyle
// @grant             GM_xmlhttpRequest
// @grant             GM_download
// @grant             GM_setClipboard
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_openInTab
// @grant             GM_info
// @grant             GM_registerMenuCommand
// ==/UserScript==

(function(){
	"use strict";
	let d = document, ver = "v.0.1.2";

	function info(msg)
	{
		let e = d.querySelector('div.ahpd-info');
		e && (e.textContent = msg);
	}

	function log()
	{
		console.log.apply(console,["[avgleHPD]"].concat(Array.from(arguments)));
	}

	function loginfo()
	{
		log.apply(console,arguments);
		info.apply(console,arguments);
	}

	function appendStylesheet(rules, id)
	{
		let e = d.createElement("style");
		if (id){
			e.id = id;
        }
		e.type = "text/css";
		e.innerHTML = rules;
		d.getElementsByTagName("head")[0].appendChild(e);
	}

	function downloadPlaylist(playlist, filename)
	{
		let a = d.querySelector('.ahpd-download');
		a.href = URL.createObjectURL(new Blob([playlist],{type:"application/x-mpegURL"}));
		a.setAttribute("download",filename);
		a.classList.remove("ahpd-hide");
	}

	function isSegmentUriEncrypted(playlist)
	{
		let a = playlist.split('\n');
		for (let i = 0 ; i < a.length ; i++){
			if (/^\s*$/.test(a[i])){
				continue;
            }
			if (a[i].charAt(0) === "#"){
				let tag = a[i];
				if (/^#EXT-X-ENDLIST/.test(tag)){
					break;
                }
				continue;
			}
			let uri = a[i];
			if (uri.includes('!')){
				return true;
            }
		}
		return false;
	}

	function decryptPlaylist(playlist, options)
	{
		let a = playlist.split('\n');
		for (let i = 0 ; i < a.length ; i++){
			if (/^\s*$/.test(a[i])){
				continue;
            }
			if (a[i].charAt(0) === "#"){
				let tag = a[i];
				if (/^#EXT-X-ENDLIST/.test(tag)){
					break;
                }
				continue;
			}
			let uri = a[i];
			if (! /^https:\/\//.test(uri)){
				options.uri = uri;
				options.decryptURI();
				if (! options.uri){
					log("can't decript uri:",uri);
					throw Error("can't decrypt uri");
				}
				a[i] = options.uri;
			}
		}
		return a.join('\n');
	}

	function main()
	{
		if (! videojs){
			throw new Error("videojs not defined");
        }
        let s=document.getElementsByTagName("meta")[2].content;
		let prevBeforeRequest = videojs.Hls.xhr.beforeRequest;
		function restoreBeforeRequest()
		{
			videojs.Hls.xhr.beforeRequest = prevBeforeRequest;
			log("restored videojs.Hls.xhr.beforeRequest");
		}
		videojs.Hls.xhr.beforeRequest = function (options) {
			log("beforeRequest:",options.uri);
			if (/\/(video)?playback/.test(options.uri)) {
				log("got target request:",options.uri);
				setTimeout(function () {
					log("hooking request callback");
					info("wating http response");
					let prevCallback = options.callback;
					options.callback = function(error,request){
						loginfo("got response");
						if (request.rawRequest.response.includes('#EXTM3U')){
							let playlist = request.rawRequest.response;
							loginfo("got hls playlist");
							if (isSegmentUriEncrypted(playlist)){
								loginfo("segment uri is encrypted");
								let newOptions = videojs.Hls.xhr.beforeRequest({uri:"!dummy"});
								if (typeof newOptions.decryptURI !== "function"){
									throw new Error("can't retrieve decryptURI function");
                                }
								log("decryptURI:\n",newOptions.decryptURI.toString());
								loginfo("decrypting uri in playlist");
								playlist = decryptPlaylist(playlist, newOptions);
								log("decrypted playlist:\n"+ playlist);
								info("decrypted playlist successfully");
								downloadPlaylist(playlist, s + ".m3u8");
							}
							else {
								log("segment uri is not encrypted");
								downloadPlaylist(playlist, s + ".m3u8");
							}
						}
						else {
							loginfo("error: can't decrypt response!");
							log("avgle-main-ah.js must already decrypt the response if the response is encrypted");
						}
						if (prevCallback){
							prevCallback(error,request);
                        }
					};
				},0);
				setTimeout(restoreBeforeRequest, 0);
			}
			return prevBeforeRequest(options);
		};
		log("hooked videojs.Hls.xhr.beforeRequest and waiting hls xhr request");
		info("Please click the close button.");
		d.querySelector("#player_3x2_container").addEventListener("click",()=>{
			info("waiting hls xhr request");
			log("the close button clicked");
		});
		log("waiting for the close button to be clicked");
	}
	try {
		if (d.querySelector(".ahpd-area")){
			alert("avgleHPD already executed");
			return;
		}
		log("avgle HLS playlist downloader "+ver);
		console.clear = function(){};
		{
			let s, e, sel = "div.container > div.row";
			if (! (e = d.querySelector(sel))){
				//log("element '"+sel+"' not found");
				//alert("avgleHPD error: "+"element '+sel+' not found");
				return;
			}
			appendStylesheet(".ahpd-area{display:flex; font-size:large; }.ahpd-ver{margin-right:5px; background-color:gold; font-weight:bold; text-align:center; vertical-align:middle; border:1px solid transparent; padding:8px 12px; width:min-content; white-space:nowrap; border-radius:4px; }.ahpd-info{margin-right:5px; background-color:beige; text-align:center; border:1px solid transparent; padding:8px 12px; width:min-content; white-space:nowrap; font-size:large; border-radius:4px; }.ahpd-download{font-weight:bold; padding:8px 12px; }.ahpd-download:hover{border:1px outset transparent; } .ahpd-hide{display:none;}");
			let area = e.insertBefore(d.createElement("div"), e.firstElementChild);
			area.className = "ahpd-area";
			e = area.appendChild(d.createElement("div"));
			e.className = "ahpd-ver";
			e.textContent = "avgleHPD " + ver;
			e = area.appendChild(d.createElement("div"));
			e.className = "ahpd-info";
			e.textContent = "avgleHPD information here";
			e = area.appendChild(d.createElement("a"));
			e.className = "btn-primary ahpd-download ahpd-hide";
			e.textContent = "Download HLS Playlist";
		}
		main();
	}
	catch(e){
		loginfo("error: " + e.message);
	}
})();