/*jshint multistr: true */
/*jshint esnext: true */
by Arthur Edelstein, 2014. BSD 3-Clause License.
This user-script for Firefox’s Greasemonkey extension lets you use popular web pages, even if Flash and the page’s JavaScript is disabled. Supported sites include:
This script can be installed from UserScripts.org or downloaded from github.
First, the UserScript preamble.
==UserScript== @name youtubeHTML5 @namespace arthuredelstein.com @description run html5 video on youtube @include http://www.youtube.com/* @include https://www.youtube.com/* @include http://twitter.com/* @include https://twitter.com/* @include http://*.twitter.com/* @include https://*.twitter.com/* @include http://*.baidu.com/* @include https://*.baidu.com/* @version 1 @grant GM_xmlhttpRequest ==/UserScript==
/*jshint multistr: true */
/*jshint esnext: true */
Enclose all in a function to avoid polluting the global namespace.
(function () {
"use strict";
Takes a string representing a key->value map, and parses it, given the expected string separating key-value pairs, and the separator between each key and value. For example, JSON maps (stripped of their curly braces) would require pairSep = “,” and kvSep = “:”.
let stringToMap = function (string, pairSep, kvSep) {
let pairs = string.split(pairSep).map(function (kv) {
return kv.split(kvSep);
});
let result = {};
for (let i = 0; i < pairs.length; ++i) {
let pair = pairs[i];
result[pair[0]] = pair[1];
}
return result;
};
Returns the query string from a URL, including the initial ‘?’.
let getQueryString = function (url) {
let parser = document.createElement('a');
parser.href = url;
return parser.search;
};
Reads the query (“search”) part of a URL and converts it to a map.
let parseQueryString = function (queryString) {
let trimmed = queryString.replace(/^\?/,"");
return stringToMap(trimmed, "&", "=");
};
Takes the query part of a URL and returns a JSON object containing the same information.
let getQueryMap = function(url) {
return parseQueryString(getQueryString(url));
};
Takes a URL and modifies (or creates) the query part with data taken from queryMap.
let setQueryMap = function(url, queryMap) {
let parser = document.createElement('a'),
queryString = "?";
parser.href = url;
for (let key in queryMap) {
queryString += key + "=" + queryMap[key] + "&";
}
parser.search = queryString;
return parser.href;
};
Takes an array of JSON objects, and returns only those objects whose with the desired value and a given key.
let pickMapsWithTag = function(arrayOfObjects, key, value) {
return arrayOfObjects.filter(function (datum) { return datum[key] == value; });
};
Copies a URL string, and, if its protocol is http, converts it to HTTPS.
let changeToHttps = function (url) {
let parser = document.createElement('a');
parser.href = url;
if (parser.protocol === "http:") {
parser.protocol = "https:";
}
return parser.href;
};
Returns true iff the URL string uses the HTTPS protocol.
let isHttps = function (url) {
let parser = document.createElement('a');
parser.href = url;
return parser.protocol === "https:";
};
Restore images, links, or other elements that ordinarily need JavaScript to work correctly.
let restoreAttribute = function (thumbnailSelector, sourceAttribute, targetAttribute) {
Array.prototype.forEach.call(document.querySelectorAll(thumbnailSelector), element => {
if (element.hasAttribute(sourceAttribute)) {
element.setAttribute(targetAttribute, element.getAttribute(sourceAttribute));
}
});
};
let baidu = function () {
restoreAttribute('img', 'data-src', 'src');
};
let flickr = function () { restoreAttribute(‘img.defer’, ‘data-defer-src’, ‘src’); };
TODO: Get this working for various kinds of streams (login, person, search, etc)
let twitter_cleanup = function() {
restoreAttribute("a.twitter-timeline-link", "data-expanded-url", "href");
restoreAttribute("a.twitter-timeline-link", "data-resolved-url-large", "href");
};
let twitter_footer = function () {
let itemList = document.querySelector("div.GridTimeline-items"),
lastItemID = itemList.getAttribute("data-max-id"),
nextItemsURL = "https://twitter.com/i/profiles/show/NickKristof/timeline?max_id=" + lastItemID;
GM_xmlhttpRequest({
method: "GET",
url: nextItemsURL,
onload: function (response) {
itemList.insertAdjacentHTML('beforeend', JSON.parse(response.responseText).items_html);
twitter_cleanup();
}
});
};
let twitter = function () {
twitter_cleanup();
twitter_footer();
};
Scrapes useful video location data and signatures from a YouTube page.
let scrapeVideoLocationData = function (bodyHTML) {
Location data can be found in JSON object literals inside an inline SCRIPT tag.
let pattern = /\"url\_encoded\_fmt\_stream\_map\"\:\ ?\"(.*?)\"/,
A series of literal maps each corresponds to a way to request a different format of the same video.
sources = bodyHTML.match(pattern)[1].split(",");
Read the data from these literal maps.
return sources.map(function (source) { return stringToMap(source, "\\u0026", "="); });
};
Scrape necessary data from the YouTube page to construct a URL that points to an HTML5 version of the video.
let extractHTML5VideoURL = function (bodyHTML) {
let chosenVideoItem = pickMapsWithTag(scrapeVideoLocationData(bodyHTML), "itag", "43")[0],
The ‘url’ tag from chosenVideoItem contains most of the URL we need for obtaining the HTML5 video. URL-decode the tag to get a pure URL, and ensure it uses HTTPS.
url = changeToHttps(decodeURIComponent(chosenVideoItem.url)),
Read the tags from this URL, in turn.
tags = getQueryMap(url);
The signature / sig tag is necessary to obtain the video.
tags.signature = tags.signature || chosenVideoItem.signature || chosenVideoItem.sig;
Return a URL that has the full complement of tags.
return setQueryMap(url, tags);
};
Embed the video element in the YouTube page and return a reference to it.
let embedVideo = function (html5VideoURL) {
Place it in the old playerAPI box.
let playerAPI = document.querySelector("div#player-api");
playerAPI.style.position = "relative";
playerAPI.innerHTML = '<div style="background-color: black; position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 99;"> \
<video id="unscripted" src="' + html5VideoURL + '" style="width: 100%; height: 100%" controls></video> \
</div>';
return document.querySelector('video#unscripted');
};
Alter the YouTube page to show its video without needing the page’s JavaScript.
let youtube = function() {
Let’s always use HTTPS in this script, to be safer. Redundant if user uses the HTTPSEverywhere plugin.
if (!isHttps(location.href)) {
location.href = changeToHttps(location.href);
}
try {
let html5VideoURL = extractHTML5VideoURL(document.body.innerHTML);
Swap old video for new HTML5 video.
let video = embedVideo(html5VideoURL);
Play the video immediately, just as YouTube does.
video.play();
} catch (e) {
console.log(e);
Never mind.
}
restoreAttribute('img', 'data-thumb', 'src');
};
Run the main function to immediately bring web page to heel.
if (location.href.contains('youtube.com')) {
youtube();
}
if (location.href.contains('baidu.com')) {
baidu();
}
if (location.href.contains(‘flickr.com’)) { flickr(); }
if (location.href.contains('twitter.com')) {
twitter();
}
Terminate enclosing function.
})();