Greasy Fork

Greasy Fork is available in English.

下载知乎视频

在知乎的视频播放器里显示下载项

当前为 2018-03-19 提交的版本,查看 最新版本

// ==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    http://greasyfork.icu/users/38953
// ==/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 = '<path d="M9.5,4 H14.5 V10 H17.8 L12,15.8 L6.2,10 H9.5 Z M6.2,18 H17.8 V20 H6.2 Z"></path>',
        svgCircle = '<circle cx="12" cy="12" r="8" fill="none" stroke-width="2" stroke="#555" />' +
            '<text x="50%" y="50%" dy=".4em" text-anchor="middle" fill="#fff" font-size="9">0</text>' +
            '<path fill="none" r="8" transform="translate(12,12)" stroke-width="2" stroke="#fff" />',
        $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);
        }
    });
})();