// ==UserScript== // @name 下载知乎视频 // @version 0.4 // @description 在知乎的视频播放器里显示下载项 // @author Chao // @include *://www.zhihu.com/* // @match *://www.zhihu.com/* // @include https://v.vzuu.com/video/* // @match https://v.vzuu.com/video/* // @connect zhihu.com // @connect vzuu.com // @grant none // @require https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js // @namespace https://greasyfork.org/users/38953 // @downloadURL none // ==/UserScript== (function () { if (window.location.host == 'www.zhihu.com') return; var saveAs = (function (view) { // IE <10 is explicitly unsupported if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { return; } var doc = view.document // only get URL when necessary in case Blob.js hasn't overridden it yet , get_URL = function () { return view.URL || view.webkitURL || view; } , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") , can_use_save_link = "download" in save_link , click = function (node) { var event = new MouseEvent("click"); node.dispatchEvent(event); } , is_safari = /constructor/i.test(view.HTMLElement) || view.safari , is_chrome_ios = /CriOS\/[\d]+/.test(navigator.userAgent) , setImmediate = view.setImmediate || view.setTimeout , throw_outside = function (ex) { setImmediate(function () { throw ex; }, 0); } , force_saveable_type = "application/octet-stream" // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to , arbitrary_revoke_timeout = 1000 * 40 // in ms , revoke = function (file) { var revoker = function () { if (typeof file === "string") { // file is an object URL get_URL().revokeObjectURL(file); } else { // file is a File file.remove(); } }; setTimeout(revoker, arbitrary_revoke_timeout); } , dispatch = function (filesaver, event_types, event) { event_types = [].concat(event_types); var i = event_types.length; while (i--) { var listener = filesaver["on" + event_types[i]]; if (typeof listener === "function") { try { listener.call(filesaver, event || filesaver); } catch (ex) { throw_outside(ex); } } } } , auto_bom = function (blob) { // prepend BOM for UTF-8 XML and text/* types (including HTML) // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); } return blob; } , FileSaver = function (blob, name, no_auto_bom) { if (!no_auto_bom) { blob = auto_bom(blob); } // First try a.download, then web filesystem, then object URLs var filesaver = this , type = blob.type , force = type === force_saveable_type , object_url , dispatch_all = function () { dispatch(filesaver, "writestart progress write writeend".split(" ")); } // on any filesys errors revert to saving with object URLs , fs_error = function () { if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { // Safari doesn't allow downloading of blob urls var reader = new FileReader(); reader.onloadend = function () { var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); var popup = view.open(url, '_blank'); if (!popup) view.location.href = url; url = undefined; // release reference before dispatching filesaver.readyState = filesaver.DONE; dispatch_all(); }; reader.readAsDataURL(blob); filesaver.readyState = filesaver.INIT; return; } // don't create more object URLs than needed if (!object_url) { object_url = get_URL().createObjectURL(blob); } if (force) { view.location.href = object_url; } else { var opened = view.open(object_url, "_blank"); if (!opened) { // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html view.location.href = object_url; } } filesaver.readyState = filesaver.DONE; dispatch_all(); revoke(object_url); } ; filesaver.readyState = filesaver.INIT; if (can_use_save_link) { object_url = get_URL().createObjectURL(blob); setImmediate(function () { save_link.href = object_url; save_link.download = name; click(save_link); dispatch_all(); revoke(object_url); filesaver.readyState = filesaver.DONE; }, 0); return; } fs_error(); } , FS_proto = FileSaver.prototype , saveAs = function (blob, name, no_auto_bom) { return new FileSaver(blob, name || blob.name || "download", no_auto_bom); } ; // IE 10+ (native saveAs) if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { return function (blob, name, no_auto_bom) { name = name || blob.name || "download"; if (!no_auto_bom) { blob = auto_bom(blob); } return navigator.msSaveOrOpenBlob(blob, name); }; } save_link.target = "_blank"; FS_proto.abort = function () { }; FS_proto.readyState = FS_proto.INIT = 0; FS_proto.WRITING = 1; FS_proto.DONE = 2; FS_proto.error = FS_proto.onwritestart = FS_proto.onprogress = FS_proto.onwrite = FS_proto.onabort = FS_proto.onerror = FS_proto.onwriteend = null; return saveAs; }( typeof self !== "undefined" && self || typeof window !== "undefined" && window || this )); var $download, svgDownload = '', svgCircle = '' + '0' + '', $menu, $menuItem, blobs = null, ratio = 0, refererBaseUrl = 'https://v.vzuu.com/video/', playlistBaseUrl = 'https://lens.zhihu.com/api/videos/', playlistId = window.location.pathname.split('/').pop(); var fileSize = function (a, b, c, d, e) { return (b = Math, c = b.log, d = 1024/*1e3*/, e = c(a) / c(d) | 0, a / b.pow(d, e)).toFixed(0) + ' ' + (e ? 'kMGTPEZY'[--e] + 'B' : 'Bytes'); }; // 重置下载图标 var resetDownloadIcon = function () { $download.find('svg:first').html(svgDownload); }; // 更新进度界面 var updateProgress = function (progress) { var r = 8, degrees = progress / 100 * 360, // 进度对应的角度值 rad = degrees * (Math.PI / 180), // 角度对应的弧度值 x = (Math.sin(rad) * r).toFixed(2), // 极坐标转换成直角坐标 y = -(Math.cos(rad) * r).toFixed(2); // 大于180度时画大角度弧,小于180度时画小角度弧,(deg > 180) ? 1 : 0 var lenghty = window.Number(degrees > 180); // path 属性 var paths = ['M', 0, -r, 'A', r, r, 0, lenghty, 1, x, y]; $download.find('svg:first > path').attr('d', paths.join(' ')); $download.find('svg:first > text').text(progress); }; // 保存视频文件 var store = function () { for (var i in blobs) { if (blobs[i] == undefined) return; } var blob = new Blob(blobs, {type: 'video/h264'}), filename = (new Date()).valueOf() + '.mp4', a, url; blobs = null; // 结束进度显示 resetDownloadIcon(); saveAs(blob, filename); return; // if (window.navigator && window.navigator.msSaveBlob) { // window.navigator.msSaveBlob(blob, filename); // } // else { // url = URL.createObjectURL(blob); // // a = document.createElement('a'); // a.href = url; // a.download = filename; // a.click(); // a = null; // // setTimeout(function () { // URL.revokeObjectURL(url); // }, 100); // } }; // 下载 m3u8 文件中的单个 ts 文件 var downloadTs = function (url, order) { fetch(url).then(function (res) { return res.blob().then(function (blob) { ratio++; updateProgress(Math.round(100 * ratio / blobs.length)); blobs[order] = blob; store(); }); }); }; // 下载 m3u8 文件 var downloadM3u8 = function (url) { $.get(url, function (res) { //console.log(res.responseText); // 代码参考 http://nuttycase.com/vidio/ var i = 0; blobs = []; ratio = 0; // 初始化进度显示 $download.find('svg:first').html(svgCircle); res.split('\n').forEach(function (line) { if (line.match(/\.ts/)) { blobs[i] = undefined; downloadTs(url.replace(/\/[^\/]+?$/, '/' + line), i++); } }); }); }; // 读取 playlist $.getJSON({ url: playlistBaseUrl + playlistId, headers: { //referer: refererBaseUrl + playlistId, authorization: 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20' // in zplayer.min.js of zhihu }, success: function (res) { var $player = $('#player'), $controlBar = $player.find('> div:first-child > div:eq(1) > div:last-child > div:first-child'), $fullScreen = $controlBar.find('> div:nth-last-of-type(1)'), $resolution = $controlBar.find('> div:nth-last-of-type(3)'), menuStyle = 'transform:none !important; left:auto !important; right:-0.5em !important;', videos = []; // 添加下载项 $download = $resolution.clone(); // 不同分辨率视频的信息 $.each(res.playlist, function (key, value) { value.name = key; videos.push(value); }); // 按大小排序 videos = videos.sort(function (v1, v2) { return v1.width == v2.width ? 0 : (v1.width > v2.width ? 1 : -1); }).reverse(); // 下载按钮文字 $download.find('button:first').html($fullScreen.clone().find('button:first').html()).find('svg').html(svgDownload); // 各分辨率菜单 $menuItem = $download.find('button:eq(1)'); $menu = $menuItem.parent().empty(); $.each(videos, function (i, value) { $menu.append($menuItem.clone().text(value.width + ' (' + fileSize(value.size) + ')').css({ width: '100%', textAlign: 'right' })); }); $download // 显示下载菜单 .on('pointerenter', function () { if (blobs == null) { $menu.parent().attr('style', menuStyle + 'opacity:1 !important; visibility:visible !important'); } }) // 隐藏下载菜单 .on('pointerleave', function () { if (blobs == null) { $menu.parent().attr('style', menuStyle); } }) // 暂停下载 .on('pointerdown', function () { return; // if (blobs != null) { // resetDownloadIcon(); // } }); // 选择下载菜单 $menu.on('pointerup', 'button', function () { var video = videos[$(this).index()]; $menu.parent().removeAttr('style'); downloadM3u8(video.play_url); }); // 添加下载项 $controlBar.append($download); } }); })();