Greasy Fork

Greasy Fork is available in English.

微博 [ 图片 | 视频 ] 下载

下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图,可以打包下载)

当前为 2019-05-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         微博 [ 图片 | 视频 ] 下载
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图,可以打包下载)
// @author       Mr.Po
// @match        https://weibo.com/*
// @match        https://www.weibo.com/*
// @require      https://code.jquery.com/jquery-1.11.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.0/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
// @connect      sinaimg.cn
// @connect      miaopai.com
// @connect      youku.com
// @connect      weibo.com
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // 是否启用调试模式
    var isDebug = false;

    //每隔 space 毫秒执行一次
    var space = 5000;

    // TODO livePhoto、直播回放

    // 添加扩展如果需要
    function addExtendIfNeed() {


        // 查找未被扩展的box
        var $uls = $("div .screen_box ul:not([class='pic_copy_extend'])");

        if ($uls.length > 0) {

            console.info("找到未扩展的box:" + $uls.length);

            $uls.each(function(i, it) {

                handlePictureIfNeed($(it));

                handleVideoIfNeed($(it));
            });

            // 批量给这些box添加已扩展标记
            $uls.addClass("pic_copy_extend");
        }
    }



    /**
     * 处理图片,如果需要
     */
    function handlePictureIfNeed($ul) {

        // 得到大图片
        var $links = getLargePhoto($ul);

        if (isDebug) {
            console.log("此Item有图:" + $links.length);
        }

        // 判断图片是否存在
        if ($links.length > 0) {

            var lp_links = getLivePhoto($ul);

            if (lp_links) {
                $links = $($links.get().concat(lp_links));
            }

            handleCopy($ul, $links);

            handleDownload($ul, $links);

            handleDownloadZip($ul, $links);
        }
    }

    /**
     * 处理视频如果需要
     * @param  {$标签对象} $ul 操作列表
     */
    function handleVideoIfNeed($ul) {

        var $box = $ul.parents(".WB_feed_detail").find(".WB_video,.WB_video_a,.li_story");

        // 不存在视频
        if ($box.length === 0) {
            return;
        }

        var type = getVideoType($box);

        var fun;

        if (type === "feedvideo") { // 短视屏(秒拍、梨视频、优酷)

            fun = function() { downloadBlowVideo($box); };

        } else if (type === "feedlive") { // 直播回放

            //TODO 暂不支持

        } else if (type === "story") { // 微博故事

            fun = function() { downloadWeiboStory($box); };

        } else {

            console.warn("未知的类型:" + type);
        }

        if (fun) {

            putButton($ul, "下载当前视频", fun);
        }
    }

    /**
     * 提取LivePhoto的地址
     * @param  {$标签对象} $owner ul或li
     * @return {字符串数组}       LivePhoto地址集,可能为null
     */
    function extractLivePhotoSrc($owner) {

        var action_data = $owner.attr("action-data");

        if (action_data) {

            var urlsRegex = action_data.match(/pic_video=([\w:,]+)/);

            if (urlsRegex) {

                var urls = urlsRegex[1].split(",").map(function(it, i) {
                    return it.split(":")[1];
                });

                return urls;
            }
        }

        return null;
    }

    /**
     * 得到视频类型
     * @param  {$标签对象} $box 视频容器
     * @return {字符串}         视频类型[video、live]
     */
    function getVideoType($box) {

        // console.log($box);

        // console.log($box.attr("action-data"));

        var typeRegex = $box.attr("action-data").match(/type=(\w+)&/);

        // console.log(typeRegex);

        return typeRegex[1];
    }

    /**
     * 添加按钮
     * @param  {$标签对象} $ul  操作列表
     * @param  {字符串} name 按钮名称
     * @param  {方法} op   按钮操作
     */
    function putButton($ul, name, op) {

        var $li = $("<li><a href='javascript:void(0)'>—> " + name + " <—</a></li>");

        $li.click(op);

        $ul.append($li);
    }

    // 处理拷贝
    function handleCopy($ul, $links) {

        putButton($ul, "复制图片链接", function() {

            var link = $links.get().map(function(it, i) {
                return it.src;
            }).join("\n");

            GM_setClipboard(link, "text");

            GM_notification("链接已复制到剪贴板!");
        });
    }

    // 处理下载
    function handleDownload($ul, $links) {

        putButton($ul, "逐个下载图片", function() {

            $links.each(function(i, it) {

                // console.log("name:" + it.name + ",src=" + it.src);

                GM_download(it.src, it.name);
            });
        });
    }

    /**
     * 处理打包下载
     */
    function handleDownloadZip($ul, $links) {

        putButton($ul, "打包下载图片", function() {

            startZip($ul, $links);
        });
    }

    /**
     * 下载微博故事
     * @param  {$标签对象} $box 视频box
     */
    function downloadWeiboStory($box) {

        var action_data = $box.attr("action-data");

        var urlRegex = action_data.match(/gif_url=([\w%.]+)&/);

        var url = urlRegex[1];

        var src = decodeURIComponent(decodeURIComponent(url));

        var name = getPathName(src.split("?")[0]);

        if (src.indexOf("//") === 0) {
            src = "https:" + src;
        }

        downloadVideo($box, name, src);
    }

    /**
     * 下载酷燃视频
     * @param  {$标签对象} $box 视频box
     */
    function downloadBlowVideo($box) {

        var src, name;

        try {

            var video_sources = $box.attr("video-sources");

            // 多清晰度源
            var sources = video_sources.split("&");

            if (isDebug) {
                console.log(sources);
            }


            // 逐步下调清晰度
            for (var i = sources.length - 2; i >= 0; i -= 2) {

                if (sources[i].trim().split("=")[1].trim().length > 0) {

                    // 解码
                    var source = decodeURIComponent(decodeURIComponent(sources[i].trim()));

                    if (isDebug) {
                        console.log(source);
                    }

                    src = source.substring(source.indexOf("=") + 1);
                }
            }

            if (!src) { // 未找到合适的视频地址

                GM_notification("未能找到视频地址!");

                throw new Error("未能找到视频地址!");
            }

            name = getPathName(src.split("?")[0]);

            if (isDebug) {
                console.log("download:" + name + "=" + src);
            }

        } catch (e) {

            console.error(e);

            GM_notification("提取视频地址失败!");
        }


        downloadVideo($box, name, src);
    }

    /**
     * 下载直播回放
     * @param  {$标签对象} $li 视频box
     */
    function downloadLiveVCRVideo($ul, $li) {
        // TODO 暂不支持
    }

    /**
     * 下载视频
     * @param  {$标签对象} $box 视频box
     */
    function downloadVideo($box, name, src) {

        GM_notification("即将开始下载...");

        var progress = bornProgress($box);

        GM_download({
            url: src,
            name: name,
            onprogress: function(p) {

                var value = p.loaded / p.total;
                progress.value = value;
            },
            onerror: function(e) {

                console.error(e);

                GM_notification("视频下载出错!");
            }
        });
    }

    /**
     * 得到LivePhoto链接集
     * @param  {$标签对象} $ul 操作列表
     * @return {Link数组}     链接集,可能为null
     */
    function getLivePhoto($ul) {

        var $box = $ul.parents(".WB_feed_detail").find(".WB_media_a");

        var srcs;

        // 仅有一张LivePhoto
        if ($box.hasClass('WB_media_a_m1')) {

            srcs = extractLivePhotoSrc($box.find(".WB_pic"));

        } else {

            srcs = extractLivePhotoSrc($box);
        }

        if (srcs) {
            srcs = srcs.map(function(it, i) {

                var src = "https://video.weibo.com/media/play?livephoto=//us.sinaimg.cn/" + it + ".mov&KID=unistore,videomovSrc";

                return bornLink(it + ".mp4", src);
            });
        }

        return srcs;
    }

    function bornLink(name, src) {
        return { name: name, src: src };
    }

    /**
     * 得到大图链接
     * @param  {$标签对象} $ul 操作列表
     * @return {Link数组}     链接集,可能为null
     */
    function getLargePhoto($ul) {

        // 得到每一个图片
        var links = $ul.parents(".WB_feed_detail").find("li.WB_pic img").map(function() {

            var parts = $(this).attr("src").split("/");

            // 替换为大图链接
            var src = "http://wx2.sinaimg.cn/large/" + parts[parts.length - 1];

            if (isDebug) {
                console.log(src);
            }

            return bornLink(getPathName(src), src);
        });

        return links;
    }

    /**
     * 得到图片名称
     * @param  {字符串} src 图片地址
     * @return {字符串}     图片名称(含后缀)
     */
    function getPathName(src) {

        var name = src.substring(src.lastIndexOf("/") + 1);

        if (isDebug) {
            console.log("截得名称为:" + name);
        }

        return name;
    }

    /**
     * 得到当前卡片的名称
     * @param  {$标签对象} $ul 操作列表
     * @return {字符串}        卡片名称
     */
    function getCardName($ul) {

        var cardName = $ul.parents("div.WB_feed_detail").find("div.WB_info a").first().text();

        if (isDebug) {
            console.log("得到的名称为:" + cardName);
        }

        return cardName;
    }

    /**
     * 生成一个进度条
     * @param  {$标签对象} $sub card的子节点
     * @param  {int}      max  最大值
     * @return {标签对象}     进度条
     */
    function bornProgress($sub) {

        var $div = $sub.parents("div.WB_feed_detail").find("div.WB_info").first();

        // 尝试获取进度条
        var $progress = $div.find('progress');

        // 进度条不存在时,生成一个
        if ($progress.length === 0) {

            $progress = $("<progress max='1' style='margin-left:10px;' />");

            $div.append($progress);

        } else { // 已存在时,重置value

            $progress.removeAttr('value');
        }

        return $progress[0];
    }

    /**
     * 开始打包
     * @param  {$数组} $links 图片地址集
     */
    function startZip($ul, $links) {

        GM_notification("正在打包,请稍候...");

        var progress = bornProgress($ul);

        var zip = new JSZip();

        var names = [];

        $links.each(function(i, it) {

            var name = it.name;

            GM_xmlhttpRequest({
                method: 'GET',
                url: it.src,
                responseType: "blob",
                onload: function(response) {

                    zip.file(name, response.response);

                    downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
                },
                onerror: function(e) {

                    console.error(e);

                    GM_notification("第" + (i + 1) + "个对象时,获取失败!");

                    downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
                }
            });
        });
    }


    function downloadZipIfComplete($ul, progress, name, zip, names, length) {

        names.push(name);

        var value = names.length / length;

        progress.value = value;

        if (names.length === length) {

            zip.generateAsync({
                    type: "blob"
                })
                .then(function(content) {

                    var cardName = getCardName($ul);

                    saveAs(content, cardName + "-" + (new Date().getTime()) + ".zip");
                });
        }
    }

    setInterval(addExtendIfNeed, space);
})();