Greasy Fork

Greasy Fork is available in English.

下载知乎视频

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

目前为 2018-05-09 提交的版本。查看 最新版本

// ==UserScript==
// @name         下载知乎视频
// @version      0.6
// @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        GM_download
// @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 $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',
            url = window.URL.createObjectURL(blob),
            userAgent = window.navigator.userAgent;

        blobs = null;

        // 结束进度显示
        resetDownloadIcon();

        if (window.navigator && window.navigator.msSaveBlob) {
            window.navigator.msSaveBlob(blob, filename);
        }
        else {
            url = window.URL.createObjectURL(blob);

            // Chrome 可以使用 GM_download 函数绕过 CSP(Content Security Policy) 的限制
            if (userAgent.indexOf('Chrome') > 0 && window.GM_download) {
                GM_download(url, filename);
            }
            else {
                // firefox 需要禁用 CSP, about:config -> security.csp.enable => false
                // violentmonkey(暴力猴)没有 GM_download 函数
                var a = document.createElement('a');
                document.body.appendChild(a);
                a.href = url;
                a.download = filename;
                //a.target = '_blank';
                a.click();
                document.body.removeChild(a);

                setTimeout(function () {
                    window.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);
        }
    });
})();