NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
/** Originally written by angelsl With contributions from Manish Burman http://mburman.com With contributions from LouCypher https://github.com/LouCypher YTGrab is distributed under the GNU LGPL v3 or later and comes with no warranty. Full preamble at https://github.com/angelsl/misc-Scripts/blob/master/Greasemonkey/LICENSE.md#ytgrab // ==UserScript== // @name YouTube Download Button // @namespace https://github.com/angelsl/misc-Scripts // @description Inserts a download button on YouTube video pages // @version 2.03 // @run-at document-end // @updateURL https://github.com/angelsl/misc-Scripts/raw/master/Greasemonkey/YTGrab.user.js // @downloadURL https://github.com/angelsl/misc-Scripts/raw/master/Greasemonkey/YTGrab.user.js // @include https://www.youtube.com/* // @include http://www.youtube.com/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js // @grant GM_xmlhttpRequest // @grant unsafeWindow // ==/UserScript== */ if (typeof unsafeWindow === 'undefined' || typeof unsafeWindow.ytplayer === 'undefined') { var p = document.createElement('p'); p.setAttribute('onclick', 'return window;'); unsafeWindow = p.onclick(); } /* The following block of code is adapted from https://github.com/gantt/downloadyoutube The MIT License (MIT) Copyright (c) 2014 gantt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ function findSignatureCode(sourceCode) { function findMatch(text, regexp) { var matches = text.match(regexp); return (matches) ? matches[1] : null; } function isString(s) { return (typeof s === 'string' || s instanceof String); } function isInteger(n) { return (typeof n === 'number' && n % 1 == 0); } var signatureFunctionName = findMatch(sourceCode, /\.set\s*\("signature"\s*,\s*([a-zA-Z0-9_$][\w$]*)\(/) || findMatch(sourceCode, /\.sig\s*\|\|\s*([a-zA-Z0-9_$][\w$]*)\(/) || findMatch(sourceCode, /\.signature\s*=\s*([a-zA-Z_$][\w$]*)\([a-zA-Z_$][\w$]*\)/); //old if (signatureFunctionName == null) return []; signatureFunctionName = signatureFunctionName.replace('$', '\\$'); var regCode = new RegExp(signatureFunctionName + '\\s*=\\s*function' + '\\s*\\([\\w$]*\\)\\s*{[\\w$]*=[\\w$]*\\.split\\(""\\);(.+);return [\\w$]*\\.join'); var regCode2 = new RegExp('function \\s*' + signatureFunctionName + '\\s*\\([\\w$]*\\)\\s*{[\\w$]*=[\\w$]*\\.split\\(""\\);(.+);return [\\w$]*\\.join'); var functionCode = findMatch(sourceCode, regCode) || findMatch(sourceCode, regCode2); if (functionCode == null) return []; var reverseFunctionName = findMatch(sourceCode, /([\w$]*)\s*:\s*function\s*\(\s*[\w$]*\s*\)\s*{\s*(?:return\s*)?[\w$]*\.reverse\s*\(\s*\)\s*}/); if (reverseFunctionName) reverseFunctionName = reverseFunctionName.replace('$', '\\$'); var sliceFunctionName = findMatch(sourceCode, /([\w$]*)\s*:\s*function\s*\(\s*[\w$]*\s*,\s*[\w$]*\s*\)\s*{\s*(?:return\s*)?[\w$]*\.(?:slice|splice)\(.+\)\s*}/); if (sliceFunctionName) sliceFunctionName = sliceFunctionName.replace('$', '\\$'); var regSlice = new RegExp('\\.(?:' + 'slice' + (sliceFunctionName ? '|' + sliceFunctionName : '') + ')\\s*\\(\\s*(?:[a-zA-Z_$][\\w$]*\\s*,)?\\s*([0-9]+)\\s*\\)'); // .slice(5) sau .Hf(a,5) var regReverse = new RegExp('\\.(?:' + 'reverse' + (reverseFunctionName ? '|' + reverseFunctionName : '') + ')\\s*\\([^\\)]*\\)'); // .reverse() sau .Gf(a,45) var regSwap = new RegExp('[\\w$]+\\s*\\(\\s*[\\w$]+\\s*,\\s*([0-9]+)\\s*\\)'); var regInline = new RegExp('[\\w$]+\\[0\\]\\s*=\\s*[\\w$]+\\[([0-9]+)\\s*%\\s*[\\w$]+\\.length\\]'); var functionCodePieces = functionCode.split(';'); var decodeArray = []; for (var i = 0; i < functionCodePieces.length; i++) { functionCodePieces[i] = functionCodePieces[i].trim(); var codeLine = functionCodePieces[i]; if (codeLine.length > 0) { var arrSlice = codeLine.match(regSlice); var arrReverse = codeLine.match(regReverse); if (arrSlice && arrSlice.length >= 2) { // slice var slice = parseInt(arrSlice[1], 10); if (isInteger(slice)) { decodeArray.push(-slice); } else return []; } else if (arrReverse && arrReverse.length >= 1) { // reverse decodeArray.push(0); } else if (codeLine.indexOf('[0]') >= 0) { // inline swap if (i + 2 < functionCodePieces.length && functionCodePieces[i + 1].indexOf('.length') >= 0 && functionCodePieces[i + 1].indexOf('[0]') >= 0) { var inline = findMatch(functionCodePieces[i + 1], regInline); inline = parseInt(inline, 10); decodeArray.push(inline); i += 2; } else return []; } else if (codeLine.indexOf(',') >= 0) { // swap var swap = findMatch(codeLine, regSwap); swap = parseInt(swap, 10); if (isInteger(swap) && swap > 0) { decodeArray.push(swap); } else return []; } else return []; } } return decodeArray; } function decryptSignature(sig, dec) { function swap(a, b) { var c = a[0]; a[0] = a[b % a.length]; a[b] = c; return a }; function decode(sig, arr) { // encoded decryption var sigA = sig.split(''); for (var i = 0; i < arr.length; i++) { var act = arr[i]; sigA = (act > 0) ? swap(sigA, act) : ((act == 0) ? sigA.reverse() : sigA.slice(-act)); } var result = sigA.join(''); return result; } if (dec) { var sig2 = decode(sig, dec); if (sig2) return sig2; } return sig; } // End adapted code function run() { if (typeof unsafeWindow.ytplayer !== 'undefined') { GM_xmlhttpRequest({ method: "GET", url: unsafeWindow.ytplayer.config.assets.js.replace(/^\/\//, "https://"), onload: function(t) { var rules = findSignatureCode(t.responseText); if (!rules || rules.length < 1) console.log("failed to get sig decode fn"); var fn = (function(u) { return decryptSignature(u, rules); }); main(fn); } }); } } function main(decipher) { var dashmpd = unsafeWindow.ytplayer.config.args.dashmpd, mpbsrgx = /\/s\/([\w\.]+)/, mpbs; if (typeof dashmpd !== 'undefined') { mpbs = mpbsrgx.exec(dashmpd); if (mpbs) dashmpd = dashmpd.replace(mpbsrgx, "/signature/" + decipher(mpbs[1])); GM_xmlhttpRequest({ method: "GET", url: dashmpd, onload: function(t) { main2(t.responseText, decipher); } }); } else { main2(false, decipher); } } function main2(dashmpd, decipher) { "use strict"; var uriencToMap = function(s) { var n = {}, a = s.split("&"), idy, c; for (idy = 0; idy < a.length; idy++) { c = a[idy].split("="); n[c[0]] = decodeURIComponent(c[1]); } return n; }, uwyca = unsafeWindow.ytplayer.config.args, title = uwyca.title.replace(/[\/\\\:\*\?\"<\>\|]/g, ""), fmtrgx = /^[\-\w+]+\/(?:x-)?([\-\w+]+)/, fmt_map = {}, idx, idz, n, a, qual, fmt, fmt_list, map, uefmss, dashlist, ul, q, div, type, itag, maporder, fpsa, fpsb, fpsw = false; fmt_list = uwyca.fmt_list.split(","); for (idx = 0; idx < fmt_list.length; idx++) { a = fmt_list[idx].split("/"); fmt_map[a[0]] = a[1].split("x")[1] + "p"; } map = {}; uefmss = uwyca.url_encoded_fmt_stream_map.split(","); for (idx = 0; idx < uefmss.length; idx++) { n = uriencToMap(uefmss[idx]); qual = fmt_map[n.itag]; if (!(qual in map)) { map[qual] = []; } fmt = fmtrgx.exec(n.type); map[qual].push($("<a>" + (fmt ? fmt[1] : "MISSINGNO.").toUpperCase() + "</a>").attr("href", n.url + ((n.url.indexOf("signature=") !== -1) ? "" : ("&signature=" + (n.sig || decipher(n.s)))) + "&title=" + title).attr("title", "Format ID: " + n.itag + " | Quality: " + n.quality + " | Mime: " + n.type)); } dashlist = uwyca.adaptive_fmts; if (typeof dashlist !== 'undefined') { dashlist = dashlist.split(","); for (idx = 0; idx < dashlist.length; idx++) { n = uriencToMap(dashlist[idx]); qual = n.type.indexOf("audio/") === 0 ? "Audio" : (("size" in n) ? (n.size.split('x')[1] + 'p' + n.fps) : (n.itag in fmt_map) ? (fmt_map[n.itag]) : ("Unknown")); if (!(qual in map)) { map[qual] = []; } fmt = fmtrgx.exec(n.type); if (parseInt(n.fps) == 1) fpsw = 1; map[qual].push($("<a>DASH" + (fmt ? fmt[1] : "MISSINGNO.").toUpperCase() + "</a>").attr("href", n.url + ((n.url.indexOf("signature=") !== -1) ? "" : ("&signature=" + (n.sig || decipher(n.s)))) + "&title=" + title).attr("title", "Format ID: " + n.itag + " | Bitrate: " + n.bitrate + " | Mime: " + n.type + " | Res: " + n.size + " | FPS: " + n.fps)); } } if (dashmpd !== false) { dashmpd = $($.parseXML(dashmpd)); dashmpd.find("AdaptationSet").each(function() { q = $(this); type = q.attr("mimeType"); q.children("Representation").each(function() { n = $(this); itag = n.attr("id"); qual = type.indexOf("audio/") === 0 ? "Audio" : (n.attr("height") + 'p' + n.attr("frameRate")); if (!(qual in map)) { map[qual] = []; } fmt = fmtrgx.exec(type); if (parseInt(n.attr("frameRate")) == 1) fpsw = 1; map[qual].push($("<a>MPD" + (fmt ? fmt[1] : "MISSINGNO.").toUpperCase() + "</a>").attr("href", n.children("BaseURL").text() + "&title=" + title).attr("title", "Format ID: " + itag + " | Bitrate: " + n.attr("bandwidth") + " | Mime: " + type + (type.indexOf("audio/") === 0 ? " | Sample Rate: " + n.attr("audioSamplingRate") : " | Res: " + n.attr("width") + 'x' + n.attr("height") + " | FPS: " + n.attr("frameRate")))); }); }); } maporder = Object.keys(map); maporder.sort(function(a, b) { if ((a == "Audio" && b == "Unknown") || (b == "Audio" && a != "Unknown")) return -1; if ((b == "Audio" && a == "Unknown") || (a == "Audio" && b != "Unknown")) return 1; fpsa = a.split('p')[1] || 0; fpsb = b.split('p')[1] || 0; if (fpsa != fpsb) return parseInt(fpsb) - parseInt(fpsa); return parseInt(b) - parseInt(a); }); ul = $("<ul class=\"watch-extras-section\" />"); for (n = 0; n < maporder.length; ++n) { q = maporder[n]; if (map[q].length < 1) { continue; } div = $("<div class=\"content\" />").append(map[q][0]); for (idz = 1; idz < map[q].length; idz++) { div.append(" ").append(map[q][idz]); } ul.append($("<li><h4 class=\"title\" style=\"font-weight: bold; color: #333333;\">" + q + "</h4></li>").append(div)); } $("#action-panel-share").after($("<div id=\"action-panel-sldownload\" class=\"action-panel-content hid\" data-panel-loaded=\"true\" />").append(ul)); $("#watch8-secondary-actions").find("> div").eq(1).after($('<button class="yt-uix-button yt-uix-button-size-default yt-uix-button-opacity action-panel-trigger yt-uix-button-opacity yt-uix-tooltip" type="button" onclick=";return false;" title="" data-trigger-for="action-panel-sldownload" data-button-toggle="true"><span class="yt-uix-button-content">Download</span></button>')).size(); if (fpsw) ul.after($("<p>You may notice that some videos have a reported FPS of 1. This is not a bug with YTGrab; YouTube is reporting this value. The actual files have a proper FPS.</p>")); } // waitForKeyElements from https://gist.github.com/BrockA/2625891 function waitForKeyElements(selectorTxt, actionFunction) { var targetNodes, btargetsFound; targetNodes = $(selectorTxt); if (targetNodes && targetNodes.length > 0) { btargetsFound = true; targetNodes.each(function() { var jThis = $(this); var alreadyFound = jThis.data('alreadyFound') || false; if (!alreadyFound) { actionFunction(jThis); jThis.data('alreadyFound', true); } }); } else { btargetsFound = false; } var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace(/[^\w]/g, "_"); var timeControl = controlObj[controlKey]; if (!timeControl) { timeControl = setInterval(function() { waitForKeyElements(selectorTxt, actionFunction); }, 300 ); controlObj[controlKey] = timeControl; } waitForKeyElements.controlObj = controlObj; }; waitForKeyElements("#watch8-secondary-actions", run);