- // ==UserScript==
- // @name 微博 [ 图片 | 视频 ] 下载
- // @namespace http://tampermonkey.net/
- // @version 2.4.6
- // @description 下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图(9+),可以打包下载)
- // @author Mr.Po
- // @match https://weibo.com/*
- // @match https://www.weibo.com/*
- // @match https://d.weibo.com/*
- // @match https://s.weibo.com/*
- // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.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
- // @require https://cdn.staticfile.org/mustache.js/3.1.0/mustache.min.js
- // @resource iconError https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/error.png
- // @resource iconSuccess https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/success.png
- // @resource iconInfo https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/info.png
- // @resource iconExtract https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/extract.png
- // @resource iconZip https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/zip.png
- // @connect sinaimg.cn
- // @connect miaopai.com
- // @connect video.qq.com
- // @connect youku.com
- // @connect weibo.com
- // @grant GM_notification
- // @grant GM_setClipboard
- // @grant GM_download
- // @grant GM_xmlhttpRequest
- // @grant GM_getResourceURL
- // @grant GM_setValue
- // @grant GM_getValue
- // ==/UserScript==
-
- // @更新日志
- // v2.4.6 2021-06-25 1、更新支持广告类视频解析。
- // v2.4.5 2021-06-08 1、修复来自腾讯的视频,无法解析的bug。
- // v2.4.4 2021-04-05 1、修复某些视频无法解析的bug。
- // v2.4.3 2020-09-18 1、更新视频链接解析方式,支持1080P+(需自身是微博会员)。
- // v2.4.2 2020-08-11 1、新增“操作提示”开关;2、更新jquery来源。
- // v2.4.1 2020-06-28 1、修复使用“resource_id”命名时,出现重复后缀的bug。
- // v2.4 2020-05-06 1、新增wb_root_*命名参数。
- // v2.3.1 2020-04-27 1、优化图标资源加载。
- // v2.3 2020-04-27 1、修复视频下载未默认最高清晰度的bug;2、修复逐个下载最多10张的bug;3、修复部分情况下,图片重复的bug。
- // v2.2 2020-01-12 1、更新9+图片解析策略。
- // v2.1 2019-12-19 1、支持9+图片下载。
- // v2.0 2019-06-23 1、重构代码逻辑;2、优化自定义命名方式。
- // v1.1 2019-05-24 1、新增支持热门微博、微博搜索;2、新增可选文件命名方式。
- // v1.0 2019-05-23 1、支持LivePhoto、短视频、动/静图,可以打包下载。
-
- (function() {
- 'use strict';
-
- /*jshint esversion: 8 */
-
- class Config {
-
- /********************* ↓ 用户可配置区域 ↓ *********************/
-
- /**
- * 媒体类型
- * 【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 此方法的返回值,影响资源名称中的 @mdeia_type 参数值
- */
- static get mediaType() {
-
- return Config.getValue("mediaType", () => {
- return {
- picture: "P",
- livePhoto: "L",
- video: "V"
- };
- });
- }
-
- /**
- * 得到资源名称
- * 【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 此方法的返回值,影响资源名称
- *
- * 默认的:${wb_user_name}-${wb_id}-${no}
- * 会生成:小米商城-4375413591293810-01
- *
- * 若改为:微博-${media_type}-${wb_user_name}-${wb_id}-${no}
- * 会生成:微博-P-小米商城-4375413591293810-01
- *
- * @param {字符串} wb_user_name 微博用户名(如:小米商城)
- * @param {字符串} wb_user_id 微博用户ID(如:5578564422)
- * @param {字符串} wb_id 微博ID(如:4375413591293810)
- * @param {字符串} wb_url 微博地址(如:1871821935_Ilt7yCnvt)https://weibo.com/
- * @param {字符串} resource_id 资源原始名称(如:0065x5rwly1g3c6exw0a2j30u012utyg)
- * @param {字符串} no 序号(如:01)
- * @param {字符串} mdeia_type 媒体类型(如:P)
- * @param {字符串} wb_root_user_name 微博根用户名
- * @param {字符串} wb_root_user_id 微博根用户ID
- * @param {字符串} wb_root_url 微博根链接
- * @param {字符串} wb_root_id 微博根ID
- *
- * @return {字符串} 由以上字符串组合而成的名称
- */
- static getResourceName(wb_user_name, wb_user_id, wb_id, wb_url,
- resource_id, no, mdeia_type,
- wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id) {
-
- const template = Config.getValue("resourceName",
- () => "{{wb_root_user_name}}-{{wb_root_id}}-{{no}}"
- );
-
- return Mustache.render(template, {
- wb_user_name: wb_user_name,
- wb_user_id: wb_user_id,
- wb_id: wb_id,
- wb_url: wb_url,
- resource_id: resource_id,
- no: no,
- mdeia_type: mdeia_type,
- wb_root_user_name: wb_root_user_name,
- wb_root_user_id: wb_root_user_id,
- wb_root_url: wb_root_url,
- wb_root_id: wb_root_id
- });
- }
-
- /**
- * 得到打包名称
- * 【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 此方法的返回值,影响打包名称
- *
- * 默认的:${wb_user_name}-${wb_id}
- * 会生成:小米商城-4375413591293810
- *
- * 若改为:压缩包-${wb_user_name}-${wb_id}
- * 会生成:压缩包-小米商城-4375413591293810
- *
- *
- * @param {字符串} wb_user_name 微博用户名(如:小米商城)
- * @param {字符串} wb_user_id 微博用户ID(如:5578564422)
- * @param {字符串} wb_id 微博ID(如:4375413591293810)
- * @param {字符串} wb_url 微博地址(如:1871821935_Ilt7yCnvt)
- * @param {字符串} wb_root_user_name 微博根用户名
- * @param {字符串} wb_root_user_id 微博根用户ID
- * @param {字符串} wb_root_url 微博根链接
- * @param {字符串} wb_root_id 微博根ID
- *
- * @return {字符串} 由以上字符串组合而成的名称
- */
- static getZipName(wb_user_name, wb_user_id, wb_id, wb_url,
- wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id) {
-
- const template = Config.getValue("zipName",
- () => "{{wb_root_user_name}}-{{wb_root_id}}"
- );
-
- return Mustache.render(template, {
- wb_user_name: wb_user_name,
- wb_user_id: wb_user_id,
- wb_id: wb_id,
- wb_url: wb_url,
- wb_root_user_name: wb_root_user_name,
- wb_root_user_id: wb_root_user_id,
- wb_root_url: wb_root_url,
- wb_root_id: wb_root_id
- });
- }
-
- /**
- * 最大等待请求时间(超时时间),单位:毫秒
- *【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 若经常请求超时,可适当增大此值
- *
- * @type {Number}
- */
- static get maxRequestTime() {
-
- return Config.getValue("maxRequestTime", () => 8000);
- }
-
- /**
- * 每隔 space 毫秒检查一次,是否有新的微博被加载出来
- *【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 此值越小,检查越快;过小会造成浏览器卡顿
- * @type {Number}
- */
- static get space() {
-
- return Config.getValue("space", () => 5000);
- }
-
- /**
- * 是否开启操作提示
- * 【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 启用后,右下角会有弹窗对操作进行反馈。
- * @type {Boolean}[true/false]
- */
- static get isTip() {
- return JSON.parse(Config.getValue("tip", () => true));
- }
-
- /********************* ↑ 用户可配置区域 ↑ *********************/
-
- /**
- * 是否启用调试模式
- * 【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * 启用后,浏览器控制台会显示此脚本运行时的调试数据
- * @type {Boolean}[true/false]
- */
- static get isDebug() {
-
- return JSON.parse(Config.getValue("debug", () => false));
- }
-
- /**
- * 已添加增强扩展的item,会追加此类
- * 【不推荐】直接在此修改数据,应前往【储存】中修改。
- *
- * @type 字符串
- */
- static get handledWeiBoCardClass() {
-
- return Config.getValue("handledWeiBoCardClass", () => "weibo_383402_extend");
- }
-
- /**
- * 得到值
- * @param {字符串} name 键
- * @param {方法} fun 缺省产生值的方法
- * @return {值} 值
- */
- static getValue(name, fun) {
-
- let value = Config.properties[name];
-
- // 本地map中不存在(此处不能用‘非’,因为false会进入)
- if (value == undefined) {
-
- value = GM_getValue(name, null);
-
- // 储存中也不存在(此处不能用‘非’,因为false会进入)
- if (value == undefined) {
-
- value = fun();
- GM_setValue(name, value);
- }
-
- // 记录到本地map中
- Config.properties[name] = value;
- }
-
- return value;
- }
- }
-
- Config.properties = new Map();
- /*jshint esversion: 6 */
-
- /**
- * 接口
- */
- class Interface {
-
- /**
- * 构造函数
- * @param {字符串} name 接口名
- * @param {字符串数组} methods 该接口所包含的所有方法
- */
- constructor(name, methods) {
-
- //判断接口的参数个数(第一个为接口对象,第二个为参数数组)
- if (arguments.length != 2) {
- throw new Error('创建的接口对象参数必须为两个,第二个为方法数组!');
- }
-
- // 判断第二个参数是否为数组
- if(!Array.isArray(methods)){
- throw new Error('参数2必须为字符串数组!');
- }
-
- //接口对象引用名
- this.name = name;
-
- //自己的属性
- this.methods = []; //定义一个内置的空数组对象 等待接受methods里的元素(方法名称)
-
- //判断数组是否中的元素是否为string的字符串
- for (var i = 0; i < methods.length; i++) {
-
- //判断方法数组里面是否为string(字符串)的属性
- if (typeof methods[i] != 'string') {
- throw new Error('方法名必须是string类型的!');
- }
-
- //把他放在接口对象中的methods中(把接口方法名放在Interface对象的数组中)
- this.methods.push(methods[i]);
- }
- }
-
- /**
- * 实现
- * @param {对象} obj 待实现接口的对象
- * @param {接口} I 接口对象
- * @param {对象} proxy 接口的实现
- * @return {对象} 扩展后的当前对象
- */
- static impl(obj, I, proxy) {
-
- if (I.constructor != Interface) {
- throw new Error("参数2不是一个接口!");
- }
-
- // 校验实现是否实现了接口的每一个方法
- for (var i = 0; i < I.methods.length; i++) {
-
- // 方法名
- var methodName = I.methods[i];
-
- //判断obj中是否实现了接口的方法和methodName是方法(而不是属性)
- if (!proxy[methodName] || typeof proxy[methodName] != 'function') {
- throw new Error('有接口的方法没实现');
- }
-
- // 将代理中的方法渡给obj
- obj[methodName] = proxy[methodName];
- }
- }
- }
- /*jshint esversion: 6 */
-
- class Link {
-
- /**
- * 构造函数
- *
- * @param {字符串} name 名称
- * @param {字符串} src 地址
- */
- constructor(name, src) {
- this.name = name;
- this.src = src;
- }
- }
- /*jshint esversion: 6 */
-
- /**
- * 微博解析器接口
- */
- const WeiBoResolver = new Interface("SearchWeiBoResolver",
- [
- "getOperationButton", // 得到操作按钮[↓]
- "getOperationList", // 根据操作按钮,得到操作列表
- "get9PhotoImgs", // 返回九宫格图片的img$控件数组(自带后缀)
- "get9PhotoOver", // 得到超过部分的图片的id数组(无后缀)
- "getLivePhotoContainer",
- "getWeiBoCard", // 这条微博(若为转发微博,则取根微博)
- "getWeiBoInfo", // 这条微博(发布者)信息
- "getRootWeiBoInfo", // 这条微博(【根】发布者)信息
- "getWeiBoId", // 此条微博的ID
- "getWeiBoUserId", // 微博发送者的Id
- "getWeiBoUserName", // 微博发送者的名称
- "getWeiBoUrl", // 此条微博的地址
- "getProgressContainer",
- "getVideoBox",
- "geiVideoSrc"
- ]);
- /*jshint esversion: 6 */
-
- /**
- * 搜索微博 - 解析器
- */
- const SearchWeiBoResolver = {};
-
- Interface.impl(SearchWeiBoResolver, WeiBoResolver, {
- getOperationButton: () => $(`div .menu a:not(.${Config.handledWeiBoCardClass})`),
- getOperationList: $operationButton => $operationButton.parents(".menu").find("ul"),
- get9PhotoImgs: $ul => $ul.parents(".card-wrap").find(".media.media-piclist img"),
- get9PhotoOver: $ul => new Promise((resolve, reject) => { resolve([]); }), //搜索微博不会展示9+图片
- getLivePhotoContainer: $ul => $(null),
- getWeiBoCard: ($ul, isRoot) => {
-
- const $content = $ul.parents(".content");
-
- const $card_content = $content.find(".card-comment");
-
- let $content_node;
-
- if ($card_content.length == 1 && isRoot) { // 这是转发微博 && 需要根
-
- $content_node = $card_content;
-
- } else {
-
- $content_node = $content;
- }
-
- return $content_node;
- },
- getWeiBoInfo: $ul => {
-
- return SearchWeiBoResolver.getWeiBoCard($ul, false).find("a.name").first();
- },
- getRootWeiBoInfo: $ul => {
-
- return SearchWeiBoResolver.getWeiBoCard($ul, true).find("a.name").first();
- },
- getWeiBoId: ($ul, $info, isRoot) => {
-
- const action_data = $info.parents(".card-wrap")
- .find(".card-act li:eq(1) a").attr("action-data");
-
- const rootmid_regex = action_data.match(/&rootmid=(\d+)&/);
-
- let mid;
-
- if (rootmid_regex && isRoot) { // 这是转发微博 && 需要根
-
- mid = rootmid_regex[1];
-
- } else {
-
- mid = action_data.match(/[&\d]mid=(\d+)&/)[1];
- }
-
- Core.log(`得到根【${isRoot}】的微博ID为:${mid}`);
-
- return mid;
- },
- getWeiBoUserId: ($ul, $info, isRoot) => {
-
- const user_id = $info.attr("href").match(/weibo\.com\/[u\/]{0,2}(\d+)/)[1].trim();
-
- Core.log(`得到根【${isRoot}】的微博用户ID为:${user_id}`);
-
- return user_id;
- },
- getWeiBoUserName: ($ul, $info, isRoot) => {
-
- let name = $info.attr("nick-name");
-
- // 不存在
- if (!name) {
- name = $info.text();
- }
-
- name = name.trim();
-
- Core.log(`得到根【${isRoot}】的名称为:${name}`);
-
- return name;
- },
- getWeiBoUrl: ($ul, isRoot) => {
-
- const $card = SearchWeiBoResolver.getWeiBoCard($ul, isRoot);
-
- const $froms = $card.find(".from");
-
- let $a;
-
- if ($froms.length == 2) { // 转发微博
-
- if (isRoot) { // 需要根
-
- $a = $($froms[0]).find("a");
-
- } else {
-
- $a = $($froms[1]).find("a");
- }
-
- } else {
-
- $a = $($froms[0]).find("a");
- }
-
- const url = $a.attr("href").match(/weibo\.com\/(\d+\/\w+)\?/)[1].trim();
-
- Core.log(`得到根【${isRoot}】微博的地址为:${url}`);
-
- return url.replace("\/", "_");
- },
- getProgressContainer: $sub => $sub.parents(".card-wrap").find("a.name").first().parent(),
- getVideoBox: $ul => $ul.parents(".card-wrap").find(".WB_video_h5").first(),
- geiVideoSrc: $box => {
-
- let src = $box.attr("action-data").match(/video_src=([\w\/\.%]+)/)[1];
-
- src = decodeURIComponent(decodeURIComponent(src));
-
- if (src.indexOf("http") != 0) {
- src = `https:${src}`;
- }
-
- return src;
- }
- });
- /*jshint esversion: 8 */
-
- /**
- * 我的微博(含:我的微博、他人微博、我的收藏、热门微博) - 解析器
- */
- const MyWeiBoResolver = {};
-
- Interface.impl(MyWeiBoResolver, WeiBoResolver, {
- getOperationButton: () => $(`div .screen_box i.ficon_arrow_down:not(.${Config.handledWeiBoCardClass})`),
- getOperationList: $operationButton => $operationButton.parents(".screen_box").find("ul"),
- get9PhotoImgs: $ul => $ul.parents(".WB_feed_detail").find("li.WB_pic img"),
- get9PhotoOver: ($ul) => {
-
- return new Promise((resolve, reject) => {
-
- const $box = $ul.parents(".WB_feed_detail").find(".WB_media_a");
-
- const action_data = $box.attr("action-data");
-
- const over9pic = action_data.match(/over9pic=1&/);
-
- if (over9pic) { // 存在超9图
-
- const ids_regex = action_data.match(/pic_ids=([\w,]+)&/);
-
- if (ids_regex) { // 得到图片ids_regex
-
- const ids = ids_regex[1].split(",");
-
- // 已知所有图片
- if (ids.length > 9) { // 用户已手动触发加载over
-
- resolve(ids.splice(9));
-
- } else { // 只知前面9张,用户未手动触发加载over
-
- Core.log("未知超过部分图片!");
-
- const mid_regex = action_data.match(/mid=([\d,]+)&/);
-
- if (mid_regex) { // 找到mid
-
- const mid = mid_regex[1];
-
- // 请求未显示的图片id
- GM_xmlhttpRequest({
- method: 'GET',
- url: `https://weibo.com/aj/mblog/getover9pic?ajwvr=6&mid=${mid}&__rnd=${Date.now()}`,
- timeout: Config.maxRequestTime,
- responseType: "json",
- onload: function(res) {
-
- resolve(res.response.data.pids);
- },
- onerror: function(e) {
-
- console.error(e);
-
- reject("请求未展示图片发生错误");
- },
- ontimeout: function() {
-
- reject("请求未展示图片超时!");
- }
- });
-
- } else { // 未找到mid
-
- reject("未能找到此条微博的mid!");
- }
- }
-
- } else {
-
- reject("获取图片ids失败!");
- }
-
- } else { // 图片数量未超9张
-
- resolve([]);
- }
- });
- },
- getLivePhotoContainer: $ul => $ul.parents(".WB_feed_detail").find(".WB_media_a"),
- getWeiBoCard: ($ul, isRoot) => {
-
- // 此条微博
- const $box = $ul.parents("div.WB_feed_detail");
-
- // 根微博
- const $box_expand = $box.find(".WB_feed_expand");
-
- let $box_node = $box;
-
- // 这是一条转发微博 && 并且需要取根
- if ($box_expand.length == 1 && isRoot) {
-
- $box_node = $box_expand;
- }
-
- return $box_node;
- },
- getWeiBoInfo: $ul => {
-
- return MyWeiBoResolver.getWeiBoCard($ul, false).find("div.WB_detail div.WB_info a").first();
- },
- getRootWeiBoInfo: $ul => {
-
- return MyWeiBoResolver.getWeiBoCard($ul, true).find("div.WB_info a").first();
- },
- getWeiBoId: ($ul, $info, isRoot) => {
-
- const id_regex = $info.attr("suda-uatrack").match(/value=\w+:(\d+)/);
-
- let id;
-
- if (id_regex) { // 我的微博、他人微博(转发)、我的收藏、热门微博
-
- id = id_regex[1].trim();
-
- } else { // 他人微博
-
- id = $ul.parents(".WB_feed_detail").parents(".WB_cardwrap").attr("mid").trim();
- }
-
- Core.log(`得到根【${isRoot}】的微博ID为:${id}`);
-
- return id;
- },
- getWeiBoUserId: ($ul, $info, isRoot) => {
-
- const user_id = $info.attr("usercard").match(/id=(\d+)/)[1].trim();
-
- Core.log(`得到根【${isRoot}】的微博用户ID为:${user_id}`);
-
- return user_id;
- },
- getWeiBoUserName: ($ul, $info, isRoot) => {
-
- // 适用于根微博
- let name = $info.attr("nick-name");
-
- // 不存在
- if (!name) {
- name = $info.text();
- }
-
- name = name.trim();
-
- Core.log(`得到根【${isRoot}】的名称为:${name}`);
-
- return name;
- },
- getWeiBoUrl: ($ul, isRoot) => {
-
- const $li_forward = $ul
- .parents(".WB_feed_detail")
- .parents("div.WB_cardwrap")
- .find(".WB_feed_handle .WB_row_line li:eq(1) a");
-
- const action_data = $li_forward.attr("action-data");
-
- const rooturl_regex = action_data.match(/rooturl=https:\/\/weibo\.com\/(\d+\/\w+)&/);
-
- let url;
-
- if (rooturl_regex && isRoot) { // 这是转发微博 && 需要根
-
- url = rooturl_regex[1].trim();
-
- } else {
-
- url = action_data.match(/&url=https:\/\/weibo\.com\/(\d+\/\w+)&/)[1].trim();
- }
-
- Core.log(`得到根【${isRoot}】微博的地址为:${url}`);
-
- return url.replace("\/", "_");
- },
- getProgressContainer: $sub => $sub.parents("div.WB_feed_detail").find("div.WB_info").first(),
- getVideoBox: $ul => $ul.parents(".WB_feed_detail").find(".WB_video,.WB_video_a,.li_story,.WB_video_h5_v2 .WB_feed_spec_pic"),
- geiVideoSrc: $box => {
-
- const video_sources = $box.attr("video-sources");
-
- // 多清晰度源
- const sources = video_sources.split("&");
-
- Core.log(sources);
-
- // 尝试从 quality_label_list 中,获取视频地址
- const sources_filter =
- sources.filter(it => it.trim().indexOf("quality_label_list") == 0);
-
- if (sources_filter != null && sources_filter.length > 0) {
-
- Core.log("尝试使用:quality_label_list,进行视频地址解析...");
-
- const quality_label_list = sources_filter[0].trim();
-
- // 解码
- const source = decodeURIComponent(quality_label_list);
-
- const json = source.substring(source.indexOf("=") + 1).trim();
-
- // 存在质量列表的值
- if (json.length > 0) {
-
- const $urls = JSON.parse(json);
-
- Core.log($urls);
-
- // 逐步下调清晰度,当前用户为未登录或非vip时,1080P+的地址为空
- for (let i = 0; i < $urls.length; i++) {
-
- const $url = $urls[i];
-
- const src = $url.url.trim();
-
- // 是一个链接
- if (src.indexOf("http") == 0) {
-
- Core.log(`得到一个有效链接,${$url.quality_label}:${src}`);
-
- return src;
- }
- }
-
- } else Core.log("仅存在quality_label_list的key,却无value!");
-
- } else console.log("无法找到quality_label_list!");
-
- Core.log("即将使用缺省方式,进行视频地址解析...");
-
- // 逐步下调清晰度【兼容旧版,防止 quality_label_list API变动,或quality_label_list的值不存在】
- for (let i = sources.length - 2; i >= 0; i--) {
-
- const source = sources[i].trim();
- const index = source.indexOf("=");
-
- const key = source.substring(0, index).trim();
- const value = source.substring(index + 1).trim();
-
- if (value.length > 0) {
-
- // 解码
- const src = decodeURIComponent(decodeURIComponent(value));
-
- // 是一个链接
- if (src.indexOf("http") == 0) {
-
- Core.log(`得到一个有效链接,${key}:${src}`);
-
- return src;
- }
- }
- }
-
- return null;
- }
- });
- /*jshint esversion: 8 */
-
- /**
- * 图片处理器(含:LivePhoto)
- */
- class PictureHandler {
-
- /**
- * 处理图片,如果需要
- */
- static async handlePictureIfNeed($ul) {
-
- const $button = Core.putButton($ul, "图片解析中...", null);
-
- try {
-
- const resolver = Core.getWeiBoResolver();
-
- const photo_9_ids = resolver.get9PhotoImgs($ul).map(function(i, it) {
-
- const parts = $(it).attr("src").split("/");
-
- return parts[parts.length - 1];
- }).get();
-
- Core.log("九宫格图片:");
- Core.log(photo_9_ids);
-
- const photo_9_over_ids = await resolver.get9PhotoOver($ul).catch(e => {
-
- Tip.error(e);
-
- return [];
- });
-
- Core.log("未展示图片:");
- Core.log(photo_9_over_ids);
-
- const photo_ids = photo_9_ids.concat(photo_9_over_ids);
-
- Core.log("总图片:");
- Core.log(photo_ids);
-
- // 得到大图片
- let $links = await PictureHandler.convertLargePhoto($ul, photo_ids);
-
- Core.log(`此Item有图:${$links.length}`);
-
- // 判断图片是否存在
- if ($links.length > 0) {
-
- // 得到LivePhoto的链接
- const lp_links = PictureHandler.getLivePhoto($ul, $links.length);
-
- // 存在LivePhoto
- if (lp_links) {
-
- $links = $($links.get().concat(lp_links));
- }
-
- Core.handleCopy($ul, $links);
-
- PictureHandler.handleDownload($ul, $links);
-
- PictureHandler.handleDownloadZip($ul, $links);
- }
- } catch (e) {
-
- console.error(e);
-
- Tip.error(e.message);
-
- Core.putButton($ul, "图片解析失败", null);
-
- } finally {
-
- Core.removeButton($ul, $button);
- }
- }
-
-
- /**
- * 提取LivePhoto的地址
- * @param {$标签对象} $owner ul或li
- * @return {字符串数组} LivePhoto地址集,可能为null
- */
- static extractLivePhotoSrc($owner) {
-
- const action_data = $owner.attr("action-data");
-
- if (action_data) {
-
- const urlsRegex = action_data.match(/pic_video=([\w:,]+)/);
-
- if (urlsRegex) {
-
- const urls = urlsRegex[1].split(",").map(function(it, i) {
- return it.split(":")[1];
- });
-
- return urls;
- }
- }
-
- return null;
- }
-
- /**
- * 得到LivePhoto链接集
- *
- * @param {$标签对象} $ul 操作列表
- * @param {整数} start 下标开始的位置
- * @return {Link数组} 链接集,可能为null
- */
- static getLivePhoto($ul, start) {
-
- const $box = Core.getWeiBoResolver().getLivePhotoContainer($ul);
-
- let srcs;
-
- // 仅有一张LivePhoto
- if ($box.hasClass('WB_media_a_m1')) {
-
- srcs = PictureHandler.extractLivePhotoSrc($box.find(".WB_pic"));
-
- } else {
-
- srcs = PictureHandler.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 = Core.getResourceName($ul, `https://weibo.com/${it}.mp4`, i + start, Config.mediaType.livePhoto);
-
- return new Link(name, src);
- });
- }
-
- return srcs;
- }
-
- // 处理下载
- static handleDownload($ul, $links) {
-
- Core.putButton($ul, "逐个下载图片", function() {
-
- PictureHandler.downloadByLinks(0, $links.length - 1, $links);
- });
- }
-
- /**
- * 通过$links进行依次下载
- *
- * @param {数字} index 下标
- * @param {数字} lastIndex 尾下标
- * @param {$数组} $links 包含名称与链接的$数组
- */
- static downloadByLinks(index, lastIndex, $links) {
-
- const it = $links[index];
-
- GM_download({
- url: it.src,
- name: it.name,
- onload: () => {
-
- if (index <= lastIndex) {
-
- PictureHandler.downloadByLinks(index + 1, lastIndex, $links);
- }
- }
- });
- }
-
- /**
- * 处理打包下载
- */
- static handleDownloadZip($ul, $links) {
-
- Core.putButton($ul, "打包下载图片", function() {
-
- ZipHandler.startZip($ul, $links);
- });
- }
-
- /**
- * 转换为大图链接
- *
- * @param {$控件} $ul 操作列表
- * @param {数组} photo_ids 图片id数组(可能无后缀)
- * @return {Link数组} 链接集,可能为null
- */
- static async convertLargePhoto($ul, photo_ids) {
-
- const server = Core.getLargeImageServer($ul);
-
- Core.log(`获取到服务器:${server}`);
-
- let photo_ids_fix = await Promise.all($(photo_ids).map(function(i, it) {
-
- return new Promise((resolve, reject) => {
-
- // 判断是否存在后缀
- if (it.indexOf(".") != -1) { // 存在
-
- resolve(it);
-
- } else { // 不存在
-
- // 请求,不打开流,只需要头信息
- GM_xmlhttpRequest({
- method: 'GET',
- url: `http://${server}.sinaimg.cn/thumb150/${it}`,
- timeout: Config.maxRequestTime,
- responseType: "blob",
- onload: function(res) {
-
- const postfix_regex = res.responseHeaders.match(/content-type: image\/(\w+)/);
-
- // 找到,且图片类型为git
- if (postfix_regex && postfix_regex[1] == "gif") {
-
- resolve(`${it}.gif`);
-
- } else { // 未找到,或图片类型为:jpeg
-
- resolve(`${it}.jpg`);
- }
- },
- onerror: function(e) {
-
- console.error(e);
-
- reject("请求图片格式发生错误!");
- },
- ontimeout: function() {
-
- reject("请求图片格式超时!");
- }
- });
- }
- }).catch(e => {
-
- Tip.error(e);
-
- return `${it}.jpg`;
- });
- }).get());
-
- // 去除重复
- photo_ids_fix = Array.from(new Set(photo_ids_fix));
-
- Core.log("总图片(fix):");
- Core.log(photo_ids_fix);
-
- return $(photo_ids_fix).map((i, it) => {
-
- // 替换为大图链接
- const src = `http://${server}.sinaimg.cn/large/${it}`;
-
- Core.log(src);
-
- const name = Core.getResourceName($ul, src, i, Config.mediaType.picture);
-
- return new Link(name, src);
- });
- }
- }
- /*jshint esversion: 8 */
-
- /**
- * 视频处理器
- */
- class VideoHandler {
-
- /**
- * 处理视频如果需要
- * @param {$标签对象} $ul 操作列表
- */
- static async handleVideoIfNeed($ul) {
-
- const $button = Core.putButton($ul, "视频解析中...", null);
-
- try {
-
- const $box = Core.getWeiBoResolver().getVideoBox($ul);
-
- // 不存在视频
- if ($box.length === 0) {
- return;
- }
-
- // 得到视频类型
- const type = VideoHandler.getVideoType($box);
-
-
- let $link;
-
- if (type === "feedvideo") { // 短视屏(秒拍、梨视频、优酷)
-
- $link = VideoHandler.getBlowVideoLink($box);
-
- } else if (type === "feedlive") { // 直播回放
-
- //TODO 暂不支持
-
- } else if (type === "story") { // 微博故事
-
- $link = VideoHandler.getWeiboStoryLink($box);
-
- } else if (type === "adFeedVideo") { // 广告视频(无清晰度选择)
-
-
- $link = VideoHandler.getAdVideoLink($box);
-
- } else {
-
- console.warn(`未知的类型:${type}`);
- }
-
- // 是否存在视频链接
- if ($link) {
-
- Core.handleCopy($ul, $([$link]));
-
- const fun = () => VideoHandler.downloadVideo($box, $link);
-
- Core.putButton($ul, "下载当前视频", fun);
- }
-
- } catch (e) {
-
- console.error(e);
-
- Tip.error(e.message);
-
- Core.putButton($ul, "视频解析失败", null);
-
- } finally {
-
- Core.removeButton($ul, $button);
- }
- }
-
- /**
- * 得到视频类型
- * @param {$标签对象} $box 视频容器
- * @return {字符串} 视频类型[video、live]
- */
- static getVideoType($box) {
-
- const typeRegex = $box.attr("action-data").match(/type=(\w+)&/);
-
- return typeRegex[1];
- }
-
- /**
- * 得到微博故事视频Link
- *
- * @param {$标签对象} $box 视频box
- *
- * @return {Link} 链接对象
- */
- static getWeiboStoryLink($box) {
-
- const action_data = $box.attr("action-data");
-
- const urlRegex = action_data.match(/gif_url=([\w%.]+)&/);
-
- const url = urlRegex[1];
-
- let src = decodeURIComponent(decodeURIComponent(url));
-
- const name = Core.getResourceName($box, src.split("?")[0], 0, Config.mediaType.video);
-
- if (src.indexOf("//") === 0) {
- src = "https:" + src;
- }
-
- return new Link(name, src);
- }
-
- /**
- * 得到广告视频Link
- * @param {$标签对象} $box 视频box
- *
- * @return {Link} 链接对象
- */
- static getAdVideoLink($box) {
-
- const src = SearchWeiBoResolver.geiVideoSrc($box);
-
- name = Core.getResourceName($box, src.split("?")[0], 0, Config.mediaType.video);
-
- Core.log(`download:${name}=${src}`);
-
- return new Link(name, src);
- }
-
- /**
- * 得到酷燃视频Link
- *
- * @param {$标签对象} $box 视频box
- *
- * @return {Link} 链接对象
- */
- static getBlowVideoLink($box) {
-
- let src, name;
-
- try {
-
- src = Core.getWeiBoResolver().geiVideoSrc($box);
-
- if (!src) { // 未找到合适的视频地址
-
- throw new Error("未能找到视频地址!");
- }
-
- name = Core.getResourceName($box, src.split("?")[0], 0, Config.mediaType.video);
-
- Core.log(`download:${name}=${src}`);
-
- } catch (e) {
-
- console.error(e);
-
- throw new Error("未能找到视频地址!");
- }
-
- return new Link(name, src);
- }
-
- /**
- * 下载直播回放
- * @param {$标签对象} $li 视频box
- */
- static downloadLiveVCRVideo($ul, $li) {
- // TODO 暂不支持
- }
-
- /**
- * 下载视频
- *
- * @param {$标签对象} $box 视频box
- * @param {$对象} $link Link对象
- */
- static downloadVideo($box, $link) {
-
- Tip.info("即将开始下载...");
-
- const progress = ZipHandler.bornProgress($box);
-
- GM_download({
- url: $link.src,
- name: $link.name,
- onprogress: function(p) {
-
- const value = p.loaded / p.total;
- progress.value = value;
- },
- onerror: function(e) {
-
- console.error(e);
-
- Tip.error("视频下载出错!");
- }
- });
- }
- }
- /*jshint esversion: 6 */
-
- class ZipHandler {
-
- /**
- * 生成一个进度条
- * @param {$标签对象} $sub card的子节点
- * @param {int} max 最大值
- * @return {标签对象} 进度条
- */
- static bornProgress($sub) {
-
- const $div = Core.getWeiBoResolver().getProgressContainer($sub);
-
- // 尝试获取进度条
- let $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 图片地址集
- */
- static startZip($ul, $links) {
-
- Tip.tip("正在提取,请稍候...", "iconExtract");
-
- const progress = ZipHandler.bornProgress($ul);
-
- const zip = new JSZip();
-
- const names = [];
-
- $links.each(function(i, it) {
-
- const name = it.name;
-
- GM_xmlhttpRequest({
- method: 'GET',
- url: it.src,
- timeout: Config.maxRequestTime,
- responseType: "blob",
- onload: function(response) {
-
- zip.file(name, response.response);
-
- ZipHandler.downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
- },
- onerror: function(e) {
-
- console.error(e);
-
- Tip.error(`第${(i + 1)}个对象,获取失败!`);
-
- ZipHandler.downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
- },
- ontimeout: function() {
-
- Tip.error(`第${(i + 1)}个对象,请求超时!`);
-
- ZipHandler.downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
- }
- });
- });
- }
-
- /**
- * 下载打包,如果完成
- */
- static downloadZipIfComplete($ul, progress, name, zip, names, length) {
-
- names.push(name);
-
- const value = names.length / length;
-
- progress.value = value;
-
- if (names.length === length) {
-
- Tip.tip("正在打包,请稍候...", "iconZip");
-
- zip.generateAsync({
- type: "blob"
- }, function(metadata) {
-
- progress.value = metadata.percent / 100;
-
- }).then(function(content) {
-
- Tip.success("打包完成,即将开始下载!");
-
- const zipName = Core.getZipName($ul);
-
- saveAs(content, `${zipName}.zip`);
- });
- }
- }
- }
- /*jshint esversion: 6 */
-
- /**
- * 提示
- */
- class Tip {
-
- static tip(text, iconName) {
-
- if (Config.isTip) {
-
- GM_notification({
- text: text,
- image: GM_getResourceURL(iconName),
- timeout: 3000,
- });
- }
- }
-
- static info(text) {
- Tip.tip(text, "iconInfo");
- }
-
- static error(text) {
- Tip.tip(text, "iconError");
- }
-
- static success(text) {
- Tip.tip(text, "iconSuccess");
- }
- }
- /*jshint esversion: 8 */
-
- /**
- * 核心
- */
- class Core {
-
- /**
- * 处理微博卡片
- */
- static handleWeiBoCard() {
-
- // 查找未被扩展的操作按钮
- const $operationButtons = Core.getWeiBoResolver().getOperationButton();
-
- // 存在未被扩展的操作按钮
- if ($operationButtons.length > 0) {
-
- console.info(`找到未被扩展的操作按钮:${$operationButtons.length}`);
-
- $operationButtons.one("click", event =>
- Core.resolveWeiBoCard($(event.currentTarget))
- );
-
- $operationButtons.addClass(Config.handledWeiBoCardClass);
- }
- }
-
- /**
- * 解析 微博卡片
- * 仅在初次点击 操作按钮[↓] 时,触发
- *
- * @param {$标签对象} $operationButton 操作按钮
- */
- static resolveWeiBoCard($operationButton) {
-
- const weiboResolver = Core.getWeiBoResolver();
-
- const $ul = weiboResolver.getOperationList($operationButton);
-
- PictureHandler.handlePictureIfNeed($ul);
- VideoHandler.handleVideoIfNeed($ul);
- }
-
- /**
- * 得到微博解析器
- */
- static getWeiBoResolver() {
-
- let resolver;
-
- // 微博搜索
- if (window.location.href.indexOf("https://s.weibo.com") === 0) {
-
- resolver = SearchWeiBoResolver;
-
- } else { // 我的微博、他人微博、我的收藏、热门微博
-
- resolver = MyWeiBoResolver;
- }
-
- return resolver;
- }
-
- /**
- * 添加按钮
- * @param {$标签对象} $ul 操作列表
- * @param {字符串} name 按钮名称
- * @param {方法} op 按钮操作
- *
- * @return {$控件} 按钮
- */
- static putButton($ul, name, op) {
-
- const $li = $(`<li><a href='javascript:void(0)'>—> ${name} <—</a></li>`);
-
- $li.click(op);
-
- $ul.append($li);
-
- return $li;
- }
-
- /**
- * 移除按钮
- * @param {$标签对象} $ul 操作列表
- * @param {$控件} $button 按钮
- */
- static removeButton($ul, $button) {
-
- $ul.find(`li a:contains(${$button.text()})`).remove();
- }
-
- /**
- * 处理拷贝
- *
- * @param {$对象} $ul 操作列表
- * @param {$数组} $links Link数组
- */
- static handleCopy($ul, $links) {
-
- Core.putButton($ul, "复制资源链接", function() {
-
- const link = $links.get().map(function(it, i) {
- return it.src;
- }).join("\n");
-
- GM_setClipboard(link, "text");
-
- Tip.success("链接地址已复制到剪贴板!");
- });
- }
-
- /**
- * 得到打包名称
- *
- * @param {$标签对象} $ul 操作列表
- * @return {字符串} 压缩包名称(不含后缀)
- */
- static getZipName($ul) {
-
- const weiBoResolver = Core.getWeiBoResolver();
-
-
- const $info = weiBoResolver.getWeiBoInfo($ul);
- const wb_id = weiBoResolver.getWeiBoId($ul, $info, false);
- const wb_user_id = weiBoResolver.getWeiBoUserId($ul, $info, false);
- const wb_user_name = weiBoResolver.getWeiBoUserName($ul, $info, false);
- const wb_url = weiBoResolver.getWeiBoUrl($ul, false);
-
- const $root_info = weiBoResolver.getRootWeiBoInfo($ul);
- const wb_root_id = weiBoResolver.getWeiBoId($ul, $root_info, true);
- const wb_root_user_id = weiBoResolver.getWeiBoUserId($ul, $root_info, true);
- const wb_root_user_name = weiBoResolver.getWeiBoUserName($ul, $root_info, true);
- const wb_root_url = weiBoResolver.getWeiBoUrl($ul, true);
-
- const name = Config.getZipName(
- wb_user_name, wb_user_id,
- wb_id, wb_url,
- wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id
- );
-
- return name;
- }
-
- /**
- * 得到资源原始名称(不含后缀)
- * @param {字符串} path 路径
- * @return {字符串} 名称(不含后缀)
- */
- static getPathName(path) {
-
- const start = path.lastIndexOf("/") + 1;
- const end = path.lastIndexOf(".");
-
- let name;
-
- if (end > start) {
-
- name = path.substring(start, end);
-
- } else {
-
- name = path.substring(start);
- }
-
- Core.log(`截得名称为:${name}`);
-
- return name;
- }
-
- /**
- * 得到后缀
- * @param {字符串} path 路径
- * @param {字符串} media_type 媒体类型
- *
- * @return {字符串} 后缀(含.)
- */
- static getPathPostfix(path, media_type) {
-
- let postfix = path.substring(path.lastIndexOf(".") + 1).toLowerCase();
-
- Core.log(`截得后缀为:${postfix}`);
-
- // 媒体类型为图片
- if (media_type == Config.mediaType.picture) {
-
- const pics = ["jpg", "jpeg", "gif", "png", "bmp", "tif"];
-
- // 此格式的后缀不是一个常见格式,可能是解析错误导致
- // 也可能就是一个冷门格式,但此格式若使用GM进行下载,则会受到限制
- if ($.inArray(postfix, pics) == -1) {
-
- console.warn(`不能识别的【${media_type}】格式:${postfix},Ta即将被覆盖为${pics[0]}。`);
-
- postfix = pics[0];
-
- }
-
- } else if (media_type == Config.mediaType.video ||
- media_type == Config.mediaType.livePhoto) { // 媒体类型为视频
-
- const vids = ["mp4", "wmv", "avi", "ts", "mov"];
-
- if ($.inArray(postfix, vids) == -1) {
-
- console.warn(`不能识别的【${media_type}】格式:${postfix},Ta即将被覆盖为${vids[0]}。`);
-
- postfix = vids[0];
- }
- }
-
- return `.${postfix}`;
- }
-
-
- /**
- * 得到资源名称
- *
- * @param {$标签对象} $ul 操作列表
- * @param {字符串} src 资源地址
- * @param {整数} index 序号
- * @param {字符串} media_type 媒体类型
- *
- * @return {字符串} 资源名称(含后缀)
- */
- static getResourceName($ul, src, index, media_type) {
-
- const weiBoResolver = Core.getWeiBoResolver();
-
- const resource_id = Core.getPathName(src);
-
-
- const $info = weiBoResolver.getWeiBoInfo($ul);
- const wb_id = weiBoResolver.getWeiBoId($ul, $info, false);
- const wb_user_id = weiBoResolver.getWeiBoUserId($ul, $info, false);
- const wb_user_name = weiBoResolver.getWeiBoUserName($ul, $info, false);
- const wb_url = weiBoResolver.getWeiBoUrl($ul, false);
-
- const $root_info = weiBoResolver.getRootWeiBoInfo($ul);
- const wb_root_id = weiBoResolver.getWeiBoId($ul, $root_info, true);
- const wb_root_user_id = weiBoResolver.getWeiBoUserId($ul, $root_info, true);
- const wb_root_user_name = weiBoResolver.getWeiBoUserName($ul, $root_info, true);
- const wb_root_url = weiBoResolver.getWeiBoUrl($ul, true);
-
- // 修正,从1开始
- index++;
-
- // 补齐位数:01、02、03...
- if (index.toString().length === 1) {
- index = "0" + index.toString();
- }
-
- const no = index;
-
- const postfix = Core.getPathPostfix(src, media_type);
-
- const name = Config.getResourceName(
- wb_user_name, wb_user_id, wb_id, wb_url,
- resource_id, no, media_type,
- wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id) + postfix;
-
- return name;
- }
-
- /**
- * 记录日志
- * @param {字符串} msg 日志内容
- */
- static log(msg) {
- if (Config.isDebug) {
- console.log(msg);
- }
- }
-
- /**
- * 得到大图服务器
- *
- * @param {$控件} $ul 操作列表
- * @return {字符串} 服务器
- */
- static getLargeImageServer($ul) {
-
- const weiBoResolver = Core.getWeiBoResolver();
-
- const $imgs = weiBoResolver.get9PhotoImgs($ul);
-
- const src = $($imgs[0]).attr("src");
-
- let server;
-
- if (src) {
-
- const server_regex = src.match(/(wx\d)\.sinaimg\.cn/);
-
- if (server_regex) {
-
- server = server_regex[1];
- }
- }
-
- if (!server) {
-
- // 缺省服务器
- server = "wx2";
- }
-
- return server;
- }
- }
- setInterval(Core.handleWeiBoCard, Config.space);
- })();