// ==UserScript==
// @name Download YouTube Videos
// @namespace http://userscripts.org/users/90251
// @description Adds a download menu to YouTube videos.
// @include http://*.youtube.com/*
// @include http://youtube.com/*
// @include https://*.youtube.com/*
// @include https://youtube.com/*
// ==/UserScript==
((function (code) {
"use strict";
// run script inside of the document so it can access JavaScript objects created by YouTube
var s = document.createElement("script");
s.appendChild(document.createTextNode("("+code.toString()+")();"));
document.head.appendChild(s);
})(function () {
"use strict";
function clickButton (event) {
var els = this.parentNode.getElementsByClassName("yt-uix-button-toggled");
for (var i = 0; i < els.length; ++ i) {
els[i].classList.remove("yt-uix-button-toggled");
}
this.classList.add("yt-uix-button-toggled");
event.preventDefault();
}
function createMenu (videos) {
var lists = [];
var types = {};
for (var i = 0; i < videos.videos.length; ++ i) {
var video = videos.videos[i];
var res = video.size ? video.size.height+'p' : 'Unknown Size';
var type = video.type.split(';')[0].split('/')[1].replace(/^x-/,'');
var url = video.url;
if (video.sig) url += "&signature="+video.sig;
var item = [video.size ? video.size.height : 0, res, url];
if (type in types) {
types[type].push(item);
}
else {
lists.push([type, (types[type] = [item])]);
}
}
lists.sort(function (lhs,rhs) {
lhs = lhs[0];
rhs = rhs[0];
return lhs < rhs ? -1 : (lhs > rhs ? 1 : 0);
});
var filename = (videos.title + (videos.user ? " by " + videos.user : "")).
replace(/["\?]/g,'').
replace(/[:;<>\*\|\/\\]/g,' ').
replace(/\s\s+/g,' ').
replace(/^\s+/,'').
replace(/\s+$/,'')+".";
var menu = document.createElement('div');
menu.id = "action-panel-download";
menu.className = "action-panel-content hid";
menu.setAttribute("data-panel-loaded","true");
menu.style.lineHeight = "1.5";
for (var i = 0; i < lists.length; ++ i) {
var type = lists[i];
var downloads = type[1];
var div = document.createElement('div');
var ul = document.createElement('ul');
var title = document.createElement('strong');
type = type[0];
title.appendChild(document.createTextNode(type));
ul.style.textAlign = "right";
ul.style.listStyle = "none";
ul.style.margin = "0";
ul.style.padding = "0";
div.style.marginRight = "20px";
div.style.display = "inline-block";
div.style.verticalAlign = "top";
downloads.sort(function (lhs,rhs) { return lhs[0] - rhs[0]; });
for (var j = 0; j < downloads.length; ++ j) {
var download = downloads[j];
var li = document.createElement('li');
var a = document.createElement('a');
a.href = download[2];
a.setAttribute("download", filename+type);
a.appendChild(document.createTextNode(download[1]));
li.appendChild(a);
ul.appendChild(li);
}
div.appendChild(title);
div.appendChild(ul);
menu.appendChild(div);
}
return menu;
}
function createMenuButton () {
var btn = document.createElement('button');
var span = document.createElement('span');
btn.type = 'button';
span.className = 'yt-uix-button-content';
span.appendChild(document.createTextNode('Download'));
btn.appendChild(span);
btn.className = 'action-panel-trigger yt-uix-button yt-uix-button-text yt-uix-button-download';
btn.addEventListener('click', clickButton, false);
btn.setAttribute('data-trigger-for','action-panel-download');
btn.setAttribute('role','button');
return btn;
}
function createSidebar (videos) {
var toolbar = document.getElementById("docs-toolbar-wrapper");
var sidebar = document.createElement('div');
var hdr = document.createElement('div');
var close = document.createElement('div');
var icon = document.createElement('div');
var img = document.createElement('div');
var details = document.createElement('div');
var menu = createMenu(videos);
sidebar.id = 'docs-details-sidebar';
sidebar.style.top = toolbar ? (toolbar.offsetTop + toolbar.offsetParent.offsetTop)+'px' : '0px';
sidebar.style.bottom = '0px';
hdr.id = 'docs-details-sidebar-header';
close.id = 'docs-details-sidebar-close';
close.tabindex = '0';
close.title = 'Close';
close.setAttribute('onclick', 'this.parentNode.parentNode.parentNode.removeChild(this.parentNode.parentNode);');
icon.className = 'docs-icon goog-inline-block ';
img.className = 'docs-icon-img-container docs-icon-img docs-icon-close-white';
details.className = 'docs-details-sidebar-details jfk-sidebar';
menu.style.marginTop = '10px';
menu.style.textAlign = 'center';
img.appendChild(document.createTextNode('\u00A0'));
icon.appendChild(img);
close.appendChild(icon);
hdr.appendChild(document.createTextNode('Download'));
hdr.appendChild(close);
sidebar.appendChild(hdr);
details.appendChild(menu);
sidebar.appendChild(details);
return sidebar;
}
function insertForm (videos) {
var actions = document.getElementById("watch7-secondary-actions");
if (!actions) {
var sidebar = document.getElementById("docs-details-sidebar");
if (sidebar) {
sidebar.parentNode.removeChild(sidebar);
}
document.body.appendChild(createSidebar(videos));
return;
}
var after = document.getElementById("watch-actions-share");
if (!after || after.parentNode != actions) {
if (actions.getElementsByClassName) {
after = actions.getElementsByClassName("clear");
after = after[after.length-1];
}
}
if (!after) {
after = actions.lastElementChild;
}
var menu = createMenu(videos);
var btn = createMenuButton();
menu.style.display = 'none';
if (after) {
actions.insertBefore(btn, after);
}
else {
actions.appendChild(btn);
}
document.getElementById('watch7-action-panels').appendChild(menu);
}
function parseVars (vars) {
vars = vars.split("&");
var map = {};
for (var i = 0; i < vars.length; ++ i) {
var v = vars[i];
var j = v.search("=");
if (j < 0) j = v.length;
var key = decodeURIComponent(v.slice(0,j).replace(/\+/g, ' '));
var value = decodeURIComponent(v.slice(j+1).replace(/\+/g, ' ') || "");
if (key in map) {
var old = map[key];
if (typeof(old) === "string") {
map[key] = [old, value];
}
else {
old.push(value);
}
}
else {
map[key] = value;
}
}
return map;
}
function findJSConfig () {
if (window.ytplayer && window.ytplayer.config) {
return window.ytplayer.config.args;
}
if (window.yt && window.yt.playerConfig) {
return window.yt.playerConfig.args;
}
var scripts = document.getElementsByTagName("script");
for (var i = scripts.length - 1; i >= 0; -- i) {
var script = scripts[i];
var code = script.textContent || script.text;
var m = /;\s*ytplayer\.config\s*=\s*(.*);\(function\(\).*\(\)\);\s*$/m.exec(code) || /\byt\.playerConfig\s*=\s*(.*);\s*$/m.exec(code);
if (m) {
try {
return JSON.parse(m[1]).args;
}
catch (e) {}
}
}
return null;
}
function findFlashConfig () {
var movie_player = document.getElementById("movie_player") || document.getElementById("vpl1");
if (!movie_player) {
return null;
}
var flashvars;
if (movie_player.nodeName === 'OBJECT') {
flashvars = movie_player.querySelector('param[name=flashvars]');
if (!flashvars) {
return null;
}
flashvars = flashvars.getAttribute('value');
}
else {
flashvars = movie_player.getAttribute('flashvars');
}
if (!flashvars) {
return null;
}
return parseVars(flashvars);
}
function parseFmtStreamMap (map, sizes) {
var videos = [];
map = map.split(',');
for (var i = 0; i < map.length; ++ i) {
var video = parseVars(map[i]);
var size = video.size;
if (size) {
size = size.split('x');
video.size = {
width: parseInt(size[0], 10),
height: parseInt(size[1], 10)
};
}
else {
video.size = sizes[video.itag];
}
videos.push(video);
}
return videos;
}
function getVideos () {
var config = findFlashConfig();
if (!config) {
config = findJSConfig();
}
if (!config || !config.url_encoded_fmt_stream_map) {
return null;
}
var fmt_list = config.fmt_list ? config.fmt_list.split(',') : [];
var sizes = {};
for (var i = 0; i < fmt_list.length; ++ i) {
var v = fmt_list[i].split('/');
var size = v[1].split('x');
sizes[v[0]] = {
width: parseInt(size[0], 10),
height: parseInt(size[1], 10)
};
}
var videos = parseFmtStreamMap(config.url_encoded_fmt_stream_map, sizes);
// these aren't really files but streams in some obscure binary format that separates audio and video
// if (config.adaptive_fmts) {
// videos.push.apply(videos, parseFmtStreamMap(config.adaptive_fmts, sizes));
// }
var title = config.title;
if (!title) {
title = document.querySelector("*[itemscope] *[itemprop='name'], head meta[name='title']");
if (title) {
title = title.getAttribute("content");
}
else {
title = config.video_id;
}
}
var user = document.querySelector("a[rel='author']");
if (user) {
user = (user.getAttribute("title") || user.textContent || user.text || '').replace(/^\s+/,'').replace(/\s+$/,'') || null;
}
if (!user) {
user = document.querySelector("*[itemscope][itemprop='author'] link[itemprop='url']");
if (user) {
user = user.getAttribute("href").split("/").pop();
}
}
return {title: title, user: user, videos: videos};
}
var retry_count = 0;
function retryLater () {
setTimeout(function () {
var videos = getVideos();
if (videos) {
insertForm(videos);
}
else if (retry_count < 5) {
++ retry_count;
retryLater();
}
}, 1000);
}
var videos = getVideos();
if (videos) {
insertForm(videos);
handleDynamicUpdates();
}
else if (!document.body) {
window.addEventListener("load",function () {
var videos = getVideos();
if (videos) {
insertForm(videos);
}
else {
retryLater();
}
handleDynamicUpdates();
},false);
}
else {
retryLater();
handleDynamicUpdates();
}
function handleDynamicUpdates () {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
if (!MutationObserver) return;
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; ++ i) {
var mutation = mutations[i];
if (mutation.type === "childList") {
for (var j = 0; j < mutation.addedNodes.length; ++ j) {
var node = mutation.addedNodes[j];
if (node.nodeType === 1 && (node.id === "watch7-container" || node.querySelector("#watch7-container"))) {
var videos = getVideos();
if (videos) {
insertForm(videos);
}
}
}
}
}
});
observer.observe(document.body, {childList: true, subtree: true});
}
}));