Adds a button that gives you a .m3u8 file for a stream on Twitch or Hitbox.
// ==UserScript== // @name Stream URL grabber // @description Adds a button that gives you a .m3u8 file for a stream on Twitch or Hitbox. // @include *://twitch.tv/* // @include *.twitch.tv/* // @include *://player.twitch.tv/* // @include *://www.player.twitch.tv/* // @include *://www.hitbox.tv/* // @include *://api.twitch.tv/*?grabber // @include *://api.twitch.tv/*&grabber // @namespace https://greasyfork.org/users/3167 // @run-at document-ready // @grant none // @version 19.1 // @license MIT // ==/UserScript== // NOTE: hls.js was previously used in this script and greasyfork now denies its use in script, so this is broken now, and i dont know if i will fix this // hls.js npm source: https://cdn.jsdelivr.net/npm/hls.js@latest if (window.top != window.self) { //don't run on frames or iframes console.log("Skipping frame/iframe..."); return; } var maxretries = 60; var debug = false; var host = window.location.host; function downloadString(text, fileType, fileName) { var blob = new Blob([text], { type: fileType }); var a = document.createElement('a'); a.download = fileName; a.href = URL.createObjectURL(blob); a.dataset.downloadurl = [fileType, a.download, a.href].join(':'); a.style.display = "none"; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function() { URL.revokeObjectURL(a.href); }, 1500); } function savem3u8(str, filename) { var sources = parsem3u8(str); var str = "#EXTM3U\n"; for (var i=0; i<sources.length; i++) { var source = sources[i]; str += "#EXTINF:123," + source.quality + "\n" str += source.source + "\n"; } downloadString(str, "m3u8", filename); } function parsem3u8(str) { console.log("parsem3u8"); var sources = []; var playlist = str.split("#EXT-X-MEDIA"); if (playlist.length>1) { for (var i=1; i<playlist.length; i++) { var entity = playlist[i]; var rows = entity.split("\n"); var quality = rows[0].split('NAME="')[1].split('"')[0]; var stream = rows[2]; var source = {quality: quality, source: stream}; sources.push(source); } } return sources; } function loadm3u8(link, cb) { console.log("loadm3u8"); var blob = null; var xhr = new XMLHttpRequest(); xhr.open("GET", link); xhr.responseType = "blob";//force the HTTP response, response-type header to be blob xhr.onload = function() { var reader = new FileReader(); reader.onload = function(event) { var str = event.target.r###lt; cb(str); }; blob = xhr.response;//xhr.response is now a blob object reader.readAsText(blob); } xhr.send(); } function playm3u8(link) { console.log("playm3u8"); var html5Player = document.getElementById('html5player'); if(Hls.isSupported()) { var config = { liveDurationInfinity: false, // true for streams initialLiveManifestSize: 3, liveSyncDurationCount: 10 // Buffer x fragments before playback }; var hls = new Hls(config); hls.loadSource(link); hls.attachMedia(html5Player); hls.on(Hls.Events.MANIFEST_PARSED,function() { html5Player.play(); }); } else if (html5Player.canPlayType('application/vnd.apple.mpegurl')) { html5Player.src = link; html5Player.addEventListener('loadedmetadata',function() { html5Player.play(); }); } } function generateM3U8Link(data) { console.log("Generating M3U8 url"); var json = data; var user; var token = json['token'] || ''; var signature = json['sig'] || ''; if (token && token!='') { var tokenjson = JSON.parse(decodeURI(token)); if (tokenjson) { user = tokenjson['channel'] || user; } } var randomp = Math.round(Math.random() * 9999999); var url = location.protocol + '//usher.ttvnw.net/api/channel/hls/' + user + '.m3u8?player=twitchweb&token=' + token + '&sig=' + signature + '&allow_audio_only=true&allow_source=true&type=any&p=' + randomp; //console.log("Encoding url..."); var urle = encodeURI(url); if (debug) { console.log("Streamgrabber: generated m3u8 url: " + urle); } return urle; } function replaceplayer(m3u8, username) { console.log("Replacing Twitch player with HTML5 player..."); var mainPlayer = null; var videoPlayer = document.getElementsByTagName('video')[0]; if (videoPlayer) { videoPlayer.pause(); mainPlayer = videoPlayer.parentElement; } if (!mainPlayer) { mainPlayer = document.getElementsByClassName('video-player__container')[0]; } if (!mainPlayer) { mainPlayer = document.getElementsByClassName('video-player')[0]; } //document.getElementsByClassName('player')[0] // // Delete the original video player, don't let it buffer in background. //document.getElementsByTagName('video')[0].src = ""; mainPlayer.innerHTML = ""; //var html = '<div id="wrap_video"><div id="video_box" style="float:left; width: 100%; height: 100%;"><div id="video_overlay" style="text-align: center; position:absolute; float:left; z-index:10; width: 100%;"></div><div><video id="html5player" style="width:100%; max-height:100vh;" controls></video></div></div></div></div>'; var html = '<video id="html5player" style="width:100%; max-height:100%;" controls></video>'; mainPlayer.innerHTML = html; mainPlayer.style.margin = "0px"; var overlay = document.getElementById('video_overlay'); var filename = username + '.m3u8'; var grabber = document.getElementById("grabber"); var sources = parsem3u8(m3u8); var controlbar = grabber.parentNode; var downloadLink = document.createElement("button"); downloadLink.innerHTML = '<span class="tw-button-icon__icon" style="cursor:pointer;">Download</span>'; downloadLink.classList.add('tw-mg-x-1'); downloadLink.classList.add('tw-button'); downloadLink.classList.add('tw-button--hollow'); downloadLink.onclick = function (event) { savem3u8(m3u8, filename); }; controlbar.appendChild(downloadLink); var selector = document.createElement("select"); //newspan.innerHTML = '<button class="tw-button tw-button--hollow"><span class="tw-button-icon__icon" style="cursor:pointer;">Grabber</span></button>'; //var selectorhtml = '<span class="tw-button-icon__icon" style="cursor:pointer;">'; var selectorhtml = ""; for (var i=0; i<sources.length; i++) { var source = sources[i]; selectorhtml += '<option value="' + source.source + '">' + source.quality + '</option>'; } //selectorhtml += '</span></select>'; selector.innerHTML = selectorhtml; //tw-interactable selector.classList.add('tw-button'); selector.classList.add('tw-button--hollow'); selector.id = "selector"; selector.onchange = function (event) { //console.log("switching stream to: " + event.target.value); playm3u8(event.target.value); } controlbar.appendChild(selector); if (typeof init_videowrapper != "undefined") { init_videowrapper(); } grabber.hidden = true; if (sources[0]) { playm3u8(sources[0].source); } } function loadgrabber(tokenurl, username) { var request = new XMLHttpRequest(); request.open('GET', tokenurl, true); request.onload = function() { if (request.status >= 200 && request.status < 400){ // Success! var data = JSON.parse(request.responseText); var m3u8link = generateM3U8Link(data); //var urle = encodeURI(m3u8link); console.log(m3u8link); loadm3u8(m3u8link, function(m3u8) { replaceplayer(m3u8, username); }); } else { // We reached our target server, but it returned an error } }; request.onerror = function() { // There was a connection error of some sort }; request.send(); } if (document.URL.split("grabber").length > 1) { console.log("Grab command detected..."); if (host == "api.twitch.tv" || host == "www.api.twitch.tv") { //var text = document.body.textContent; var text = document.body.innerText; if (document.body.children.length>0) { text = document.body.children[0].innerText; } var json = JSON.parse(text); var user = document.URL.split("api.twitch.tv/api/channels/")[1].split("/")[0].split("#")[0]; var token = json['token'] || ''; var signature = json['sig'] || ''; if (token && token!='') { var tokenjson = JSON.parse(decodeURI(token)); if (tokenjson) { user = tokenjson['channel'] || user; } } var randomp = Math.round(Math.random() * 9999999); var url = location.protocol + '//usher.ttvnw.net/api/channel/hls/' + user + '.m3u8?player=twitchweb&token=' + token + '&sig=' + signature + '&allow_audio_only=true&allow_source=true&type=any&p=' + randomp; if (debug) { console.log("Encoding url..."); } var urle = encodeURI(url); if (debug) { console.log(urle); } loadm3u8(urle, function(m3u8) { handlem3u8(m3u8, user); }); if (debug) { console.log("Streamgrabber: stream grabbed on: " + host); } } //return; // } else { var hook = function(retries) { var loaded = document.getElementById("grabber"); if (retries > 0 && !loaded) { retries--; if (debug) { console.log("Streamgrabber: hook retries: " + retries); } if (host.split("twitch.tv").length > 1) { var div = document.querySelectorAll('.channel-actions')[0]; if (div == null) { div = document.querySelectorAll('.channel-info-bar__action-container')[0]; if (div) { div = div.children[0]; } } if (div == null) { div = document.querySelectorAll('.cn-metabar__more')[0]; } if (div == null) { div = document.querySelectorAll('.channel-header__right')[0]; } if (div == null) { setTimeout(function() { hook(retries); }, 1000); return; } else { //do var username = document.URL.split("twitch.tv/")[1].split("/")[0]; var clientid = "rp5xf0lwwskmtt1nyuee68mgd0hthrw"; var url = location.protocol + '//api.twitch.tv/api/channels/' + username + '/access_token?client_id=' + clientid + '&grabber'; //var url = 'http://api.twitch.tv/api/channels/' + user + '/access_token?client_id=' + clientid + '&grabber'; var isvideo = document.URL.split("/v/")[1]; if (isvideo != undefined) { url = location.protocol + '//player.twitch.tv/?!branding&!channelInfo&video=v' + isvideo + '&client_id=' + clientid + '&grabber'; //url = 'http://player.twitch.tv/?!branding&!channelInfo&video=v' + isvideo + '&client_id=' + clientid + '&grabber'; } var newspan = document.createElement("span"); newspan.innerHTML = '<button class="tw-button tw-button--hollow"><span class="tw-button-icon__icon" style="cursor:pointer;">Grabber</span></button>'; newspan.classList.add('tw-mg-x-1'); newspan.id = "grabber"; div.appendChild(newspan); newspan.onclick = function (event) { loadgrabber(url, username); }; console.log("Streamgrabber: loaded on: " + host); } } if (host == "www.hitbox.tv") { var div = document.querySelectorAll('.status')[0]; if (div == null) { setTimeout(function() { hook(retries); }, 1000); return } else { //do var user = document.URL.split("hitbox.tv/")[1].split("/")[0]; var url = location.protocol + '//api.hitbox.tv/player/hls/' + user + '.m3u8'; var newspan = document.createElement("span"); newspan.innerHTML = '<span style="cursor:pointer;">Grabber</span>'; newspan.id = "grabber"; div.appendChild(newspan); newspan.onclick = function (event) { window.open(url); }; console.log("Streamgrabber: " + url); console.log("Streamgrabber: loaded on: " + host); } } } if (loaded) { if (debug) { console.log("Streamgrabber: already hooked"); } } else { if (host.split("twitch.tv").length > 1) { var targetNode = document.getElementsByTagName("main")[0]; if (targetNode && !targetNode.grabbed) { var config = { childList: true }; var callback = function(mutationsList) { if (debug) { console.log("Mutation detected, hooking..."); } hook(maxretries); }; var observer = new MutationObserver(callback); observer.observe(targetNode, config); targetNode.grabbed = true; } } } }; if (debug) { console.log("Streamgrabber: hooking"); } hook(maxretries); }