🏠 Home 

Stream URL grabber

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