Greasy Fork

Greasy Fork is available in English.

微博 [ 图片 | 视频 ] 下载

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         微博 [ 图片 | 视频 ] 下载
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图,可以打包下载)
// @author       Mr.Po
// @match        https://weibo.com/*
// @match        https://www.weibo.com/*
// @match        https://d.weibo.com/*
// @match        https://s.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
// @resource iconError https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/error.png
// @resource iconSuccess https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/success.png
// @resource iconInfo https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/info.png
// @resource iconExtract https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/extract.png
// @resource iconZip https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/zip.png
// @connect      sinaimg.cn
// @connect      miaopai.com
// @connect      youku.com
// @connect      weibo.com
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceURL
// ==/UserScript==

(function() {
    'use strict';

    // TODO 直播回放

    /**
     * 资源命名策略
     *
     * 0:资源原始名称(如:0065x5rwly1g3c6exw0a2j30u012utyg.jpg)
     * 1:微博用户名-微博ID-序号(如:小米商城-4375413591293810-01.jpg)[缺省]
     * 2:微博用户ID-微博ID-序号(如:5578564422-4375413591293810-01.jpg)
     * 
     * @type 整数
     */
    var resourceNamingStrategy = 1;

    /**
     * 打包命名策略
     * 
     * 1:微博用户名-微博ID(如:小米商城-4375413591293810.zip)[缺省]
     * 2:微博用户ID-微博ID(如:5578564422-4375413591293810.zip)
     * 
     * @type 整数
     */
    var zipNamingStrategy = 1;

    /**
     * 命名连接符
     * 即:“微博用户名-微博ID-序号”,中的短横线“-”
     * @type {String}
     */
    var nameingSeparator = "-";

    /**
     * 最大等待请求时间(超时时间),单位:毫秒
     * 若经常请求超时,可适当增大此值
     * 
     * @type {Number}
     */
    var maxRequestTime = 8000;

    /**
     * 每隔 space 毫秒检查一次,是否有新的微博被加载出来
     * 此值越小,检查越快;过小会造成浏览器卡顿
     * @type {Number}
     */
    var space = 5000;

    /**
     * 是否启用调试模式
     * 启用后,浏览器控制台会显示此脚本运行时的调试数据
     * @type {Boolean}
     */
    var isDebug = false;

    var handledWeiBoCardClass = "weibo_383402_extend";

    /**
     * 搜索微博解析器
     * 
     * @type {Object}
     */
    var searchWeiBoResolver = {
        getOperationList: function() {
            return $("div .menu ul:not([class='" + handledWeiBoCardClass + "'])");
        },
        getPhoto: function($ul) {
            return $ul.parents(".card-wrap").find(".media.media-piclist img");
        },
        getLivePhotoContainer: function($ul) {
            return $(null);
        },
        getWeiBoId: function($ul) {

            var mid = $ul.parents(".card-wrap").attr("mid").trim();

            return mid;
        },
        getWeiBoUserId: function($ul) {

            var $a = $ul.parents(".card-wrap").find("a.name").first();

            var id = $a.attr("href").match(/weibo\.com\/(\d+)/)[1].trim();

            if (isDebug) {
                console.log("得到的微博ID为:" + id);
            }

            return id;
        },
        getWeiBoUserName: function($ul) {

            var name = $ul.parents(".card-wrap").find("a.name").first().text().trim();

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

            return name;
        },
        getProgressContainer: function($sub) {
            return $sub.parents(".card-wrap").find("a.name").first().parent();
        },
        getVideoBox: function($ul) {
            return $ul.parents(".card-wrap").find(".WB_video_h5").first();
        },
        geiVideoSrc: function($box) {

            var src = $box.attr("action-data").match(/video_src=([\w\/\.%]+)/)[1];

            src = decodeURIComponent(decodeURIComponent(src));

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

            return src;
        }
    };

    /**
     * 我的微博解析器(含:我的微博、他人微博、我的收藏、热门微博)
     * 
     * @type {Object}
     */
    var myWeiBoResolver = {
        getOperationList: function() {
            return $("div .screen_box ul:not([class='" + handledWeiBoCardClass + "'])");
        },
        getPhoto: function($ul) {
            return $ul.parents(".WB_feed_detail").find("li.WB_pic img");
        },
        getLivePhotoContainer: function($ul) {
            return $ul.parents(".WB_feed_detail").find(".WB_media_a");
        },
        getWeiBoId: function($ul) {

            var mid = $ul.parents(".WB_cardwrap").attr("mid").trim();

            return mid;
        },
        getWeiBoUserId: function($ul) {

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

            var id = $a.attr("usercard").match(/id=(\d+)/)[1].trim();

            if (isDebug) {
                console.log("得到的微博ID为:" + id);
            }

            return id;
        },
        getWeiBoUserName: function($ul) {

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

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

            return name;
        },
        getProgressContainer: function($sub) {
            return $sub.parents("div.WB_feed_detail").find("div.WB_info").first();
        },
        getVideoBox: function($ul) {
            return $ul.parents(".WB_feed_detail").find(".WB_video,.WB_video_a,.li_story");
        },
        geiVideoSrc: function($box) {

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

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

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

            var src;

            // 逐步下调清晰度
            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);
                }
            }

            return src;
        }
    };

    /**
     * 处理微博卡片
     */
    function handleWeiBoCard() {

        // 查找未被扩展的box
        var $uls = getWeiBoResolver().getOperationList();

        if ($uls.length > 0) {

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

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

                handlePictureIfNeed($(it));

                handleVideoIfNeed($(it));
            });

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


    /**
     * 得到操作列表
     * @return {$标签对象}          操作列表
     */
    function getWeiBoResolver() {

        var resolver;

        // 微博搜索
        if (window.location.href.indexOf("https://s.weibo.com") === 0) {

            resolver = searchWeiBoResolver;

        } else { // 我的微博、他人微博、我的收藏、热门微博

            resolver = myWeiBoResolver;
        }

        return resolver;
    }

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

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

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

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

            // 得到LivePhoto的链接
            var lp_links = getLivePhoto($ul, $links.length);

            // 存在LivePhoto
            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 = getWeiBoResolver().getVideoBox($ul);

        // 不存在视频
        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");

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

    // 处理下载
    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 = getResourceName($box, src.split("?")[0], 0);

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

        downloadVideo($box, name, src);
    }

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

        var src, name;

        try {

            src = getWeiBoResolver().geiVideoSrc($box);

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

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

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

            name = getResourceName($box, src.split("?")[0], 0);

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

        } catch (e) {

            console.error(e);

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


        downloadVideo($box, name, src);
    }

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

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

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

        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);

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

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

        var $box = getWeiBoResolver().getLivePhotoContainer($ul);

        var srcs;

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

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

        } else {

            srcs = extractLivePhotoSrc($box);
        }

        // 判断是否存在LivePhoto的链接
        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";

                var name = getResourceName($ul, "https://weibo.com/" + it + ".mp4", i + start);

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

        return srcs;
    }

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

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

        // 得到每一个图片
        var links = getWeiBoResolver().getPhoto($ul).map(function(i, it) {

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

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

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

            var name = getResourceName($ul, src, i);

            return bornLink(name, src);
        });

        return links;
    }

    /**
     * 得到打包名称
     * 
     * @param  {$标签对象} $ul      操作列表
     * @return {字符串}             压缩包名称(不含后缀)
     */
    function getZipName($ul) {

        var name;

        var weiBoResolver = getWeiBoResolver();

        // 2:微博用户ID-微博ID(如:5578564422-4375413591293810.zip)
        if (zipNamingStrategy === 2) {

            name = weiBoResolver.getWeiBoUserId($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul);

        } else { // 1:微博用户名-微博ID(如:小米商城-4375413591293810.zip)[缺省]

            name = weiBoResolver.getWeiBoUserName($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul);
        }

        return name;
    }

    /**
     * 得到资源名称
     * 
     * @param  {$标签对象} $ul      操作列表
     * @param  {字符串}    src      资源地址
     * @param  {整数}      index    序号
     * @return {字符串}             资源名称(含后缀)
     */
    function getResourceName($ul, src, index) {

        var name;

        // 0:资源原始名称(如:0065x5rwly1g3c6exw0a2j30u012utyg.jpg)
        if (resourceNamingStrategy === 0) {

            name = getPathName(src);

        } else {

            // 修正,从1开始
            index++;

            // 补齐位数:01、02、03...
            if (index.toString().length === 1) {
                index = "0" + index.toString();
            }

            var postfix = getPathPostfix(src);

            var weiBoResolver = getWeiBoResolver();

            // 2:微博用户ID-微博ID-序号(如:5578564422-4375413591293810-01.jpg)
            if (resourceNamingStrategy == 2) {

                name = weiBoResolver.getWeiBoUserId($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul) + nameingSeparator + index + postfix;

            } else { // 1:微博用户名-微博ID-序号(如:小米商城-4375413591293810-01.jpg)[缺省]

                name = weiBoResolver.getWeiBoUserName($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul) + nameingSeparator + index + postfix;

            }
        }

        return name;
    }

    /**
     * 得到后缀
     * @param  {字符串} path 路径
     * @return {字符串}     后缀(含.)
     */
    function getPathPostfix(path) {

        var postfix = path.substring(path.lastIndexOf("."));

        if (isDebug) {
            console.log("截得后缀为:" + postfix);
        }

        return postfix;
    }

    /**
     * 得到资源原始名称
     * @param  {字符串} path 路径
     * @return {字符串}     名称(含后缀)
     */
    function getPathName(path) {

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

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

        return name;
    }





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

        var $div = getWeiBoResolver().getProgressContainer($sub);

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

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

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

            $div.append($progress);

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

            $progress[0].value = 0;
        }

        return $progress[0];
    }

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

        tip("正在提取,请稍候...", "iconExtract");

        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,
                timeout: maxRequestTime,
                responseType: "blob",
                onload: function(response) {

                    zip.file(name, response.response);

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

                    console.error(e);

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

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

                    tipError("第" + (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) {

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

            zip.generateAsync({
                type: "blob"
            }, function(metadata) {

                progress.value = metadata.percent / 100;

            }).then(function(content) {

                tipSuccess("打包完成,即将开始下载!");

                var zipName = getZipName($ul);

                saveAs(content, zipName + ".zip");
            });
        }
    }

    function tip(text, iconName) {
        GM_notification({
            text: text,
            image: GM_getResourceURL(iconName)
        });
    }

    function tipInfo(text) {
        tip(text, "iconInfo");
    }

    function tipError(text) {
        tip(text, "iconError");
    }

    function tipSuccess(text) {
        tip(text, "iconSuccess");
    }

    setInterval(handleWeiBoCard, space);
})();