微博 [ 图片 | 视频 ] 下载

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

  1. // ==UserScript==
  2. // @name 微博 [ 图片 | 视频 ] 下载
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.4.6
  5. // @description 下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图(9+),可以打包下载)
  6. // @author Mr.Po
  7. // @match https://weibo.com/*
  8. // @match https://www.weibo.com/*
  9. // @match https://d.weibo.com/*
  10. // @match https://s.weibo.com/*
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.0/jszip.min.js
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
  14. // @require https://cdn.staticfile.org/mustache.js/3.1.0/mustache.min.js
  15. // @resource iconError https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/error.png
  16. // @resource iconSuccess https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/success.png
  17. // @resource iconInfo https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/info.png
  18. // @resource iconExtract https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/extract.png
  19. // @resource iconZip https://cdn.jsdelivr.net/gh/Mr-Po/weibo-resource-download/out/media/zip.png
  20. // @connect sinaimg.cn
  21. // @connect miaopai.com
  22. // @connect video.qq.com
  23. // @connect youku.com
  24. // @connect weibo.com
  25. // @grant GM_notification
  26. // @grant GM_setClipboard
  27. // @grant GM_download
  28. // @grant GM_xmlhttpRequest
  29. // @grant GM_getResourceURL
  30. // @grant GM_setValue
  31. // @grant GM_getValue
  32. // ==/UserScript==
  33.  
  34. // @更新日志
  35. // v2.4.6 2021-06-25 1、更新支持广告类视频解析。
  36. // v2.4.5 2021-06-08 1、修复来自腾讯的视频,无法解析的bug。
  37. // v2.4.4 2021-04-05 1、修复某些视频无法解析的bug。
  38. // v2.4.3 2020-09-18 1、更新视频链接解析方式,支持1080P+(需自身是微博会员)。
  39. // v2.4.2 2020-08-11 1、新增“操作提示”开关;2、更新jquery来源。
  40. // v2.4.1 2020-06-28 1、修复使用“resource_id”命名时,出现重复后缀的bug。
  41. // v2.4 2020-05-06 1、新增wb_root_*命名参数。
  42. // v2.3.1 2020-04-27 1、优化图标资源加载。
  43. // v2.3 2020-04-27 1、修复视频下载未默认最高清晰度的bug;2、修复逐个下载最多10张的bug;3、修复部分情况下,图片重复的bug。
  44. // v2.2 2020-01-12 1、更新9+图片解析策略。
  45. // v2.1 2019-12-19 1、支持9+图片下载。
  46. // v2.0 2019-06-23 1、重构代码逻辑;2、优化自定义命名方式。
  47. // v1.1 2019-05-24 1、新增支持热门微博、微博搜索;2、新增可选文件命名方式。
  48. // v1.0 2019-05-23 1、支持LivePhoto、短视频、动/静图,可以打包下载。
  49.  
  50. (function() {
  51. 'use strict';
  52.  
  53. /*jshint esversion: 8 */
  54.  
  55. class Config {
  56.  
  57. /********************* ↓ 用户可配置区域 ↓ *********************/
  58.  
  59. /**
  60. * 媒体类型
  61. * 【不推荐】直接在此修改数据,应前往【储存】中修改。
  62. *
  63. * 此方法的返回值,影响资源名称中的 @mdeia_type 参数值
  64. */
  65. static get mediaType() {
  66.  
  67. return Config.getValue("mediaType", () => {
  68. return {
  69. picture: "P",
  70. livePhoto: "L",
  71. video: "V"
  72. };
  73. });
  74. }
  75.  
  76. /**
  77. * 得到资源名称
  78. * 【不推荐】直接在此修改数据,应前往【储存】中修改。
  79. *
  80. * 此方法的返回值,影响资源名称
  81. *
  82. * 默认的:${wb_user_name}-${wb_id}-${no}
  83. * 会生成:小米商城-4375413591293810-01
  84. *
  85. * 若改为:微博-${media_type}-${wb_user_name}-${wb_id}-${no}
  86. * 会生成:微博-P-小米商城-4375413591293810-01
  87. *
  88. * @param {字符串} wb_user_name 微博用户名(如:小米商城)
  89. * @param {字符串} wb_user_id 微博用户ID(如:5578564422)
  90. * @param {字符串} wb_id 微博ID(如:4375413591293810)
  91. * @param {字符串} wb_url 微博地址(如:1871821935_Ilt7yCnvt)https://weibo.com/
  92. * @param {字符串} resource_id 资源原始名称(如:0065x5rwly1g3c6exw0a2j30u012utyg)
  93. * @param {字符串} no 序号(如:01)
  94. * @param {字符串} mdeia_type 媒体类型(如:P)
  95. * @param {字符串} wb_root_user_name 微博根用户名
  96. * @param {字符串} wb_root_user_id 微博根用户ID
  97. * @param {字符串} wb_root_url 微博根链接
  98. * @param {字符串} wb_root_id 微博根ID
  99. *
  100. * @return {字符串} 由以上字符串组合而成的名称
  101. */
  102. static getResourceName(wb_user_name, wb_user_id, wb_id, wb_url,
  103. resource_id, no, mdeia_type,
  104. wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id) {
  105.  
  106. const template = Config.getValue("resourceName",
  107. () => "{{wb_root_user_name}}-{{wb_root_id}}-{{no}}"
  108. );
  109.  
  110. return Mustache.render(template, {
  111. wb_user_name: wb_user_name,
  112. wb_user_id: wb_user_id,
  113. wb_id: wb_id,
  114. wb_url: wb_url,
  115. resource_id: resource_id,
  116. no: no,
  117. mdeia_type: mdeia_type,
  118. wb_root_user_name: wb_root_user_name,
  119. wb_root_user_id: wb_root_user_id,
  120. wb_root_url: wb_root_url,
  121. wb_root_id: wb_root_id
  122. });
  123. }
  124.  
  125. /**
  126. * 得到打包名称
  127. * 【不推荐】直接在此修改数据,应前往【储存】中修改。
  128. *
  129. * 此方法的返回值,影响打包名称
  130. *
  131. * 默认的:${wb_user_name}-${wb_id}
  132. * 会生成:小米商城-4375413591293810
  133. *
  134. * 若改为:压缩包-${wb_user_name}-${wb_id}
  135. * 会生成:压缩包-小米商城-4375413591293810
  136. *
  137. *
  138. * @param {字符串} wb_user_name 微博用户名(如:小米商城)
  139. * @param {字符串} wb_user_id 微博用户ID(如:5578564422)
  140. * @param {字符串} wb_id 微博ID(如:4375413591293810)
  141. * @param {字符串} wb_url 微博地址(如:1871821935_Ilt7yCnvt)
  142. * @param {字符串} wb_root_user_name 微博根用户名
  143. * @param {字符串} wb_root_user_id 微博根用户ID
  144. * @param {字符串} wb_root_url 微博根链接
  145. * @param {字符串} wb_root_id 微博根ID
  146. *
  147. * @return {字符串} 由以上字符串组合而成的名称
  148. */
  149. static getZipName(wb_user_name, wb_user_id, wb_id, wb_url,
  150. wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id) {
  151.  
  152. const template = Config.getValue("zipName",
  153. () => "{{wb_root_user_name}}-{{wb_root_id}}"
  154. );
  155.  
  156. return Mustache.render(template, {
  157. wb_user_name: wb_user_name,
  158. wb_user_id: wb_user_id,
  159. wb_id: wb_id,
  160. wb_url: wb_url,
  161. wb_root_user_name: wb_root_user_name,
  162. wb_root_user_id: wb_root_user_id,
  163. wb_root_url: wb_root_url,
  164. wb_root_id: wb_root_id
  165. });
  166. }
  167.  
  168. /**
  169. * 最大等待请求时间(超时时间),单位:毫秒
  170. *【不推荐】直接在此修改数据,应前往【储存】中修改。
  171. *
  172. * 若经常请求超时,可适当增大此值
  173. *
  174. * @type {Number}
  175. */
  176. static get maxRequestTime() {
  177.  
  178. return Config.getValue("maxRequestTime", () => 8000);
  179. }
  180.  
  181. /**
  182. * 每隔 space 毫秒检查一次,是否有新的微博被加载出来
  183. *【不推荐】直接在此修改数据,应前往【储存】中修改。
  184. *
  185. * 此值越小,检查越快;过小会造成浏览器卡顿
  186. * @type {Number}
  187. */
  188. static get space() {
  189.  
  190. return Config.getValue("space", () => 5000);
  191. }
  192.  
  193. /**
  194. * 是否开启操作提示
  195. * 【不推荐】直接在此修改数据,应前往【储存】中修改。
  196. *
  197. * 启用后,右下角会有弹窗对操作进行反馈。
  198. * @type {Boolean}[true/false]
  199. */
  200. static get isTip() {
  201. return JSON.parse(Config.getValue("tip", () => true));
  202. }
  203.  
  204. /********************* ↑ 用户可配置区域 ↑ *********************/
  205.  
  206. /**
  207. * 是否启用调试模式
  208. * 【不推荐】直接在此修改数据,应前往【储存】中修改。
  209. *
  210. * 启用后,浏览器控制台会显示此脚本运行时的调试数据
  211. * @type {Boolean}[true/false]
  212. */
  213. static get isDebug() {
  214.  
  215. return JSON.parse(Config.getValue("debug", () => false));
  216. }
  217.  
  218. /**
  219. * 已添加增强扩展的item,会追加此类
  220. * 【不推荐】直接在此修改数据,应前往【储存】中修改。
  221. *
  222. * @type 字符串
  223. */
  224. static get handledWeiBoCardClass() {
  225.  
  226. return Config.getValue("handledWeiBoCardClass", () => "weibo_383402_extend");
  227. }
  228.  
  229. /**
  230. * 得到值
  231. * @param {字符串} name 键
  232. * @param {方法} fun 缺省产生值的方法
  233. * @return {值} 值
  234. */
  235. static getValue(name, fun) {
  236.  
  237. let value = Config.properties[name];
  238.  
  239. // 本地map中不存在(此处不能用‘非’,因为false会进入)
  240. if (value == undefined) {
  241.  
  242. value = GM_getValue(name, null);
  243.  
  244. // 储存中也不存在(此处不能用‘非’,因为false会进入)
  245. if (value == undefined) {
  246.  
  247. value = fun();
  248. GM_setValue(name, value);
  249. }
  250.  
  251. // 记录到本地map中
  252. Config.properties[name] = value;
  253. }
  254.  
  255. return value;
  256. }
  257. }
  258.  
  259. Config.properties = new Map();
  260. /*jshint esversion: 6 */
  261.  
  262. /**
  263. * 接口
  264. */
  265. class Interface {
  266.  
  267. /**
  268. * 构造函数
  269. * @param {字符串} name 接口名
  270. * @param {字符串数组} methods 该接口所包含的所有方法
  271. */
  272. constructor(name, methods) {
  273.  
  274. //判断接口的参数个数(第一个为接口对象,第二个为参数数组)
  275. if (arguments.length != 2) {
  276. throw new Error('创建的接口对象参数必须为两个,第二个为方法数组!');
  277. }
  278.  
  279. // 判断第二个参数是否为数组
  280. if(!Array.isArray(methods)){
  281. throw new Error('参数2必须为字符串数组!');
  282. }
  283.  
  284. //接口对象引用名
  285. this.name = name;
  286.  
  287. //自己的属性
  288. this.methods = []; //定义一个内置的空数组对象 等待接受methods里的元素(方法名称)
  289.  
  290. //判断数组是否中的元素是否为string的字符串
  291. for (var i = 0; i < methods.length; i++) {
  292.  
  293. //判断方法数组里面是否为string(字符串)的属性
  294. if (typeof methods[i] != 'string') {
  295. throw new Error('方法名必须是string类型的!');
  296. }
  297.  
  298. //把他放在接口对象中的methods中(把接口方法名放在Interface对象的数组中)
  299. this.methods.push(methods[i]);
  300. }
  301. }
  302.  
  303. /**
  304. * 实现
  305. * @param {对象} obj 待实现接口的对象
  306. * @param {接口} I 接口对象
  307. * @param {对象} proxy 接口的实现
  308. * @return {对象} 扩展后的当前对象
  309. */
  310. static impl(obj, I, proxy) {
  311.  
  312. if (I.constructor != Interface) {
  313. throw new Error("参数2不是一个接口!");
  314. }
  315.  
  316. // 校验实现是否实现了接口的每一个方法
  317. for (var i = 0; i < I.methods.length; i++) {
  318.  
  319. // 方法名
  320. var methodName = I.methods[i];
  321.  
  322. //判断obj中是否实现了接口的方法和methodName是方法(而不是属性)
  323. if (!proxy[methodName] || typeof proxy[methodName] != 'function') {
  324. throw new Error('有接口的方法没实现');
  325. }
  326.  
  327. // 将代理中的方法渡给obj
  328. obj[methodName] = proxy[methodName];
  329. }
  330. }
  331. }
  332. /*jshint esversion: 6 */
  333.  
  334. class Link {
  335.  
  336. /**
  337. * 构造函数
  338. *
  339. * @param {字符串} name 名称
  340. * @param {字符串} src 地址
  341. */
  342. constructor(name, src) {
  343. this.name = name;
  344. this.src = src;
  345. }
  346. }
  347. /*jshint esversion: 6 */
  348.  
  349. /**
  350. * 微博解析器接口
  351. */
  352. const WeiBoResolver = new Interface("SearchWeiBoResolver",
  353. [
  354. "getOperationButton", // 得到操作按钮[↓]
  355. "getOperationList", // 根据操作按钮,得到操作列表
  356. "get9PhotoImgs", // 返回九宫格图片的img$控件数组(自带后缀)
  357. "get9PhotoOver", // 得到超过部分的图片的id数组(无后缀)
  358. "getLivePhotoContainer",
  359. "getWeiBoCard", // 这条微博(若为转发微博,则取根微博)
  360. "getWeiBoInfo", // 这条微博(发布者)信息
  361. "getRootWeiBoInfo", // 这条微博(【根】发布者)信息
  362. "getWeiBoId", // 此条微博的ID
  363. "getWeiBoUserId", // 微博发送者的Id
  364. "getWeiBoUserName", // 微博发送者的名称
  365. "getWeiBoUrl", // 此条微博的地址
  366. "getProgressContainer",
  367. "getVideoBox",
  368. "geiVideoSrc"
  369. ]);
  370. /*jshint esversion: 6 */
  371.  
  372. /**
  373. * 搜索微博 - 解析器
  374. */
  375. const SearchWeiBoResolver = {};
  376.  
  377. Interface.impl(SearchWeiBoResolver, WeiBoResolver, {
  378. getOperationButton: () => $(`div .menu a:not(.${Config.handledWeiBoCardClass})`),
  379. getOperationList: $operationButton => $operationButton.parents(".menu").find("ul"),
  380. get9PhotoImgs: $ul => $ul.parents(".card-wrap").find(".media.media-piclist img"),
  381. get9PhotoOver: $ul => new Promise((resolve, reject) => { resolve([]); }), //搜索微博不会展示9+图片
  382. getLivePhotoContainer: $ul => $(null),
  383. getWeiBoCard: ($ul, isRoot) => {
  384.  
  385. const $content = $ul.parents(".content");
  386.  
  387. const $card_content = $content.find(".card-comment");
  388.  
  389. let $content_node;
  390.  
  391. if ($card_content.length == 1 && isRoot) { // 这是转发微博 && 需要根
  392.  
  393. $content_node = $card_content;
  394.  
  395. } else {
  396.  
  397. $content_node = $content;
  398. }
  399.  
  400. return $content_node;
  401. },
  402. getWeiBoInfo: $ul => {
  403.  
  404. return SearchWeiBoResolver.getWeiBoCard($ul, false).find("a.name").first();
  405. },
  406. getRootWeiBoInfo: $ul => {
  407.  
  408. return SearchWeiBoResolver.getWeiBoCard($ul, true).find("a.name").first();
  409. },
  410. getWeiBoId: ($ul, $info, isRoot) => {
  411.  
  412. const action_data = $info.parents(".card-wrap")
  413. .find(".card-act li:eq(1) a").attr("action-data");
  414.  
  415. const rootmid_regex = action_data.match(/&rootmid=(\d+)&/);
  416.  
  417. let mid;
  418.  
  419. if (rootmid_regex && isRoot) { // 这是转发微博 && 需要根
  420.  
  421. mid = rootmid_regex[1];
  422.  
  423. } else {
  424.  
  425. mid = action_data.match(/[&\d]mid=(\d+)&/)[1];
  426. }
  427.  
  428. Core.log(`得到根【${isRoot}】的微博ID为:${mid}`);
  429.  
  430. return mid;
  431. },
  432. getWeiBoUserId: ($ul, $info, isRoot) => {
  433.  
  434. const user_id = $info.attr("href").match(/weibo\.com\/[u\/]{0,2}(\d+)/)[1].trim();
  435.  
  436. Core.log(`得到根【${isRoot}】的微博用户ID为:${user_id}`);
  437.  
  438. return user_id;
  439. },
  440. getWeiBoUserName: ($ul, $info, isRoot) => {
  441.  
  442. let name = $info.attr("nick-name");
  443.  
  444. // 不存在
  445. if (!name) {
  446. name = $info.text();
  447. }
  448.  
  449. name = name.trim();
  450.  
  451. Core.log(`得到根【${isRoot}】的名称为:${name}`);
  452.  
  453. return name;
  454. },
  455. getWeiBoUrl: ($ul, isRoot) => {
  456.  
  457. const $card = SearchWeiBoResolver.getWeiBoCard($ul, isRoot);
  458.  
  459. const $froms = $card.find(".from");
  460.  
  461. let $a;
  462.  
  463. if ($froms.length == 2) { // 转发微博
  464.  
  465. if (isRoot) { // 需要根
  466.  
  467. $a = $($froms[0]).find("a");
  468.  
  469. } else {
  470.  
  471. $a = $($froms[1]).find("a");
  472. }
  473.  
  474. } else {
  475.  
  476. $a = $($froms[0]).find("a");
  477. }
  478.  
  479. const url = $a.attr("href").match(/weibo\.com\/(\d+\/\w+)\?/)[1].trim();
  480.  
  481. Core.log(`得到根【${isRoot}】微博的地址为:${url}`);
  482.  
  483. return url.replace("\/", "_");
  484. },
  485. getProgressContainer: $sub => $sub.parents(".card-wrap").find("a.name").first().parent(),
  486. getVideoBox: $ul => $ul.parents(".card-wrap").find(".WB_video_h5").first(),
  487. geiVideoSrc: $box => {
  488.  
  489. let src = $box.attr("action-data").match(/video_src=([\w\/\.%]+)/)[1];
  490.  
  491. src = decodeURIComponent(decodeURIComponent(src));
  492.  
  493. if (src.indexOf("http") != 0) {
  494. src = `https:${src}`;
  495. }
  496.  
  497. return src;
  498. }
  499. });
  500. /*jshint esversion: 8 */
  501.  
  502. /**
  503. * 我的微博(含:我的微博、他人微博、我的收藏、热门微博) - 解析器
  504. */
  505. const MyWeiBoResolver = {};
  506.  
  507. Interface.impl(MyWeiBoResolver, WeiBoResolver, {
  508. getOperationButton: () => $(`div .screen_box i.ficon_arrow_down:not(.${Config.handledWeiBoCardClass})`),
  509. getOperationList: $operationButton => $operationButton.parents(".screen_box").find("ul"),
  510. get9PhotoImgs: $ul => $ul.parents(".WB_feed_detail").find("li.WB_pic img"),
  511. get9PhotoOver: ($ul) => {
  512.  
  513. return new Promise((resolve, reject) => {
  514.  
  515. const $box = $ul.parents(".WB_feed_detail").find(".WB_media_a");
  516.  
  517. const action_data = $box.attr("action-data");
  518.  
  519. const over9pic = action_data.match(/over9pic=1&/);
  520.  
  521. if (over9pic) { // 存在超9图
  522.  
  523. const ids_regex = action_data.match(/pic_ids=([\w,]+)&/);
  524.  
  525. if (ids_regex) { // 得到图片ids_regex
  526.  
  527. const ids = ids_regex[1].split(",");
  528.  
  529. // 已知所有图片
  530. if (ids.length > 9) { // 用户已手动触发加载over
  531.  
  532. resolve(ids.splice(9));
  533.  
  534. } else { // 只知前面9张,用户未手动触发加载over
  535.  
  536. Core.log("未知超过部分图片!");
  537.  
  538. const mid_regex = action_data.match(/mid=([\d,]+)&/);
  539.  
  540. if (mid_regex) { // 找到mid
  541.  
  542. const mid = mid_regex[1];
  543.  
  544. // 请求未显示的图片id
  545. GM_xmlhttpRequest({
  546. method: 'GET',
  547. url: `https://weibo.com/aj/mblog/getover9pic?ajwvr=6&mid=${mid}&__rnd=${Date.now()}`,
  548. timeout: Config.maxRequestTime,
  549. responseType: "json",
  550. onload: function(res) {
  551.  
  552. resolve(res.response.data.pids);
  553. },
  554. onerror: function(e) {
  555.  
  556. console.error(e);
  557.  
  558. reject("请求未展示图片发生错误");
  559. },
  560. ontimeout: function() {
  561.  
  562. reject("请求未展示图片超时!");
  563. }
  564. });
  565.  
  566. } else { // 未找到mid
  567.  
  568. reject("未能找到此条微博的mid!");
  569. }
  570. }
  571.  
  572. } else {
  573.  
  574. reject("获取图片ids失败!");
  575. }
  576.  
  577. } else { // 图片数量未超9张
  578.  
  579. resolve([]);
  580. }
  581. });
  582. },
  583. getLivePhotoContainer: $ul => $ul.parents(".WB_feed_detail").find(".WB_media_a"),
  584. getWeiBoCard: ($ul, isRoot) => {
  585.  
  586. // 此条微博
  587. const $box = $ul.parents("div.WB_feed_detail");
  588.  
  589. // 根微博
  590. const $box_expand = $box.find(".WB_feed_expand");
  591.  
  592. let $box_node = $box;
  593.  
  594. // 这是一条转发微博 && 并且需要取根
  595. if ($box_expand.length == 1 && isRoot) {
  596.  
  597. $box_node = $box_expand;
  598. }
  599.  
  600. return $box_node;
  601. },
  602. getWeiBoInfo: $ul => {
  603.  
  604. return MyWeiBoResolver.getWeiBoCard($ul, false).find("div.WB_detail div.WB_info a").first();
  605. },
  606. getRootWeiBoInfo: $ul => {
  607.  
  608. return MyWeiBoResolver.getWeiBoCard($ul, true).find("div.WB_info a").first();
  609. },
  610. getWeiBoId: ($ul, $info, isRoot) => {
  611.  
  612. const id_regex = $info.attr("suda-uatrack").match(/value=\w+:(\d+)/);
  613.  
  614. let id;
  615.  
  616. if (id_regex) { // 我的微博、他人微博(转发)、我的收藏、热门微博
  617.  
  618. id = id_regex[1].trim();
  619.  
  620. } else { // 他人微博
  621.  
  622. id = $ul.parents(".WB_feed_detail").parents(".WB_cardwrap").attr("mid").trim();
  623. }
  624.  
  625. Core.log(`得到根【${isRoot}】的微博ID为:${id}`);
  626.  
  627. return id;
  628. },
  629. getWeiBoUserId: ($ul, $info, isRoot) => {
  630.  
  631. const user_id = $info.attr("usercard").match(/id=(\d+)/)[1].trim();
  632.  
  633. Core.log(`得到根【${isRoot}】的微博用户ID为:${user_id}`);
  634.  
  635. return user_id;
  636. },
  637. getWeiBoUserName: ($ul, $info, isRoot) => {
  638.  
  639. // 适用于根微博
  640. let name = $info.attr("nick-name");
  641.  
  642. // 不存在
  643. if (!name) {
  644. name = $info.text();
  645. }
  646.  
  647. name = name.trim();
  648.  
  649. Core.log(`得到根【${isRoot}】的名称为:${name}`);
  650.  
  651. return name;
  652. },
  653. getWeiBoUrl: ($ul, isRoot) => {
  654.  
  655. const $li_forward = $ul
  656. .parents(".WB_feed_detail")
  657. .parents("div.WB_cardwrap")
  658. .find(".WB_feed_handle .WB_row_line li:eq(1) a");
  659.  
  660. const action_data = $li_forward.attr("action-data");
  661.  
  662. const rooturl_regex = action_data.match(/rooturl=https:\/\/weibo\.com\/(\d+\/\w+)&/);
  663.  
  664. let url;
  665.  
  666. if (rooturl_regex && isRoot) { // 这是转发微博 && 需要根
  667.  
  668. url = rooturl_regex[1].trim();
  669.  
  670. } else {
  671.  
  672. url = action_data.match(/&url=https:\/\/weibo\.com\/(\d+\/\w+)&/)[1].trim();
  673. }
  674.  
  675. Core.log(`得到根【${isRoot}】微博的地址为:${url}`);
  676.  
  677. return url.replace("\/", "_");
  678. },
  679. getProgressContainer: $sub => $sub.parents("div.WB_feed_detail").find("div.WB_info").first(),
  680. getVideoBox: $ul => $ul.parents(".WB_feed_detail").find(".WB_video,.WB_video_a,.li_story,.WB_video_h5_v2 .WB_feed_spec_pic"),
  681. geiVideoSrc: $box => {
  682.  
  683. const video_sources = $box.attr("video-sources");
  684.  
  685. // 多清晰度源
  686. const sources = video_sources.split("&");
  687.  
  688. Core.log(sources);
  689.  
  690. // 尝试从 quality_label_list 中,获取视频地址
  691. const sources_filter =
  692. sources.filter(it => it.trim().indexOf("quality_label_list") == 0);
  693.  
  694. if (sources_filter != null && sources_filter.length > 0) {
  695.  
  696. Core.log("尝试使用:quality_label_list,进行视频地址解析...");
  697.  
  698. const quality_label_list = sources_filter[0].trim();
  699.  
  700. // 解码
  701. const source = decodeURIComponent(quality_label_list);
  702.  
  703. const json = source.substring(source.indexOf("=") + 1).trim();
  704.  
  705. // 存在质量列表的值
  706. if (json.length > 0) {
  707.  
  708. const $urls = JSON.parse(json);
  709.  
  710. Core.log($urls);
  711.  
  712. // 逐步下调清晰度,当前用户为未登录或非vip时,1080P+的地址为空
  713. for (let i = 0; i < $urls.length; i++) {
  714.  
  715. const $url = $urls[i];
  716.  
  717. const src = $url.url.trim();
  718.  
  719. // 是一个链接
  720. if (src.indexOf("http") == 0) {
  721.  
  722. Core.log(`得到一个有效链接,${$url.quality_label}:${src}`);
  723.  
  724. return src;
  725. }
  726. }
  727.  
  728. } else Core.log("仅存在quality_label_list的key,却无value!");
  729.  
  730. } else console.log("无法找到quality_label_list!");
  731.  
  732. Core.log("即将使用缺省方式,进行视频地址解析...");
  733.  
  734. // 逐步下调清晰度【兼容旧版,防止 quality_label_list API变动,或quality_label_list的值不存在】
  735. for (let i = sources.length - 2; i >= 0; i--) {
  736.  
  737. const source = sources[i].trim();
  738. const index = source.indexOf("=");
  739.  
  740. const key = source.substring(0, index).trim();
  741. const value = source.substring(index + 1).trim();
  742.  
  743. if (value.length > 0) {
  744.  
  745. // 解码
  746. const src = decodeURIComponent(decodeURIComponent(value));
  747.  
  748. // 是一个链接
  749. if (src.indexOf("http") == 0) {
  750.  
  751. Core.log(`得到一个有效链接,${key}:${src}`);
  752.  
  753. return src;
  754. }
  755. }
  756. }
  757.  
  758. return null;
  759. }
  760. });
  761. /*jshint esversion: 8 */
  762.  
  763. /**
  764. * 图片处理器(含:LivePhoto)
  765. */
  766. class PictureHandler {
  767.  
  768. /**
  769. * 处理图片,如果需要
  770. */
  771. static async handlePictureIfNeed($ul) {
  772.  
  773. const $button = Core.putButton($ul, "图片解析中...", null);
  774.  
  775. try {
  776.  
  777. const resolver = Core.getWeiBoResolver();
  778.  
  779. const photo_9_ids = resolver.get9PhotoImgs($ul).map(function(i, it) {
  780.  
  781. const parts = $(it).attr("src").split("/");
  782.  
  783. return parts[parts.length - 1];
  784. }).get();
  785.  
  786. Core.log("九宫格图片:");
  787. Core.log(photo_9_ids);
  788.  
  789. const photo_9_over_ids = await resolver.get9PhotoOver($ul).catch(e => {
  790.  
  791. Tip.error(e);
  792.  
  793. return [];
  794. });
  795.  
  796. Core.log("未展示图片:");
  797. Core.log(photo_9_over_ids);
  798.  
  799. const photo_ids = photo_9_ids.concat(photo_9_over_ids);
  800.  
  801. Core.log("总图片:");
  802. Core.log(photo_ids);
  803.  
  804. // 得到大图片
  805. let $links = await PictureHandler.convertLargePhoto($ul, photo_ids);
  806.  
  807. Core.log(`此Item有图:${$links.length}`);
  808.  
  809. // 判断图片是否存在
  810. if ($links.length > 0) {
  811.  
  812. // 得到LivePhoto的链接
  813. const lp_links = PictureHandler.getLivePhoto($ul, $links.length);
  814.  
  815. // 存在LivePhoto
  816. if (lp_links) {
  817.  
  818. $links = $($links.get().concat(lp_links));
  819. }
  820.  
  821. Core.handleCopy($ul, $links);
  822.  
  823. PictureHandler.handleDownload($ul, $links);
  824.  
  825. PictureHandler.handleDownloadZip($ul, $links);
  826. }
  827. } catch (e) {
  828.  
  829. console.error(e);
  830.  
  831. Tip.error(e.message);
  832.  
  833. Core.putButton($ul, "图片解析失败", null);
  834.  
  835. } finally {
  836.  
  837. Core.removeButton($ul, $button);
  838. }
  839. }
  840.  
  841.  
  842. /**
  843. * 提取LivePhoto的地址
  844. * @param {$标签对象} $owner ul或li
  845. * @return {字符串数组} LivePhoto地址集,可能为null
  846. */
  847. static extractLivePhotoSrc($owner) {
  848.  
  849. const action_data = $owner.attr("action-data");
  850.  
  851. if (action_data) {
  852.  
  853. const urlsRegex = action_data.match(/pic_video=([\w:,]+)/);
  854.  
  855. if (urlsRegex) {
  856.  
  857. const urls = urlsRegex[1].split(",").map(function(it, i) {
  858. return it.split(":")[1];
  859. });
  860.  
  861. return urls;
  862. }
  863. }
  864.  
  865. return null;
  866. }
  867.  
  868. /**
  869. * 得到LivePhoto链接集
  870. *
  871. * @param {$标签对象} $ul 操作列表
  872. * @param {整数} start 下标开始的位置
  873. * @return {Link数组} 链接集,可能为null
  874. */
  875. static getLivePhoto($ul, start) {
  876.  
  877. const $box = Core.getWeiBoResolver().getLivePhotoContainer($ul);
  878.  
  879. let srcs;
  880.  
  881. // 仅有一张LivePhoto
  882. if ($box.hasClass('WB_media_a_m1')) {
  883.  
  884. srcs = PictureHandler.extractLivePhotoSrc($box.find(".WB_pic"));
  885.  
  886. } else {
  887.  
  888. srcs = PictureHandler.extractLivePhotoSrc($box);
  889. }
  890.  
  891. // 判断是否存在LivePhoto的链接
  892. if (srcs) {
  893.  
  894. srcs = srcs.map(function(it, i) {
  895.  
  896. var src = `https://video.weibo.com/media/play?livephoto=//us.sinaimg.cn/${it}.mov&KID=unistore,videomovSrc`;
  897.  
  898. var name = Core.getResourceName($ul, `https://weibo.com/${it}.mp4`, i + start, Config.mediaType.livePhoto);
  899.  
  900. return new Link(name, src);
  901. });
  902. }
  903.  
  904. return srcs;
  905. }
  906.  
  907. // 处理下载
  908. static handleDownload($ul, $links) {
  909.  
  910. Core.putButton($ul, "逐个下载图片", function() {
  911.  
  912. PictureHandler.downloadByLinks(0, $links.length - 1, $links);
  913. });
  914. }
  915.  
  916. /**
  917. * 通过$links进行依次下载
  918. *
  919. * @param {数字} index 下标
  920. * @param {数字} lastIndex 尾下标
  921. * @param {$数组} $links 包含名称与链接的$数组
  922. */
  923. static downloadByLinks(index, lastIndex, $links) {
  924.  
  925. const it = $links[index];
  926.  
  927. GM_download({
  928. url: it.src,
  929. name: it.name,
  930. onload: () => {
  931.  
  932. if (index <= lastIndex) {
  933.  
  934. PictureHandler.downloadByLinks(index + 1, lastIndex, $links);
  935. }
  936. }
  937. });
  938. }
  939.  
  940. /**
  941. * 处理打包下载
  942. */
  943. static handleDownloadZip($ul, $links) {
  944.  
  945. Core.putButton($ul, "打包下载图片", function() {
  946.  
  947. ZipHandler.startZip($ul, $links);
  948. });
  949. }
  950.  
  951. /**
  952. * 转换为大图链接
  953. *
  954. * @param {$控件} $ul 操作列表
  955. * @param {数组} photo_ids 图片id数组(可能无后缀)
  956. * @return {Link数组} 链接集,可能为null
  957. */
  958. static async convertLargePhoto($ul, photo_ids) {
  959.  
  960. const server = Core.getLargeImageServer($ul);
  961.  
  962. Core.log(`获取到服务器:${server}`);
  963.  
  964. let photo_ids_fix = await Promise.all($(photo_ids).map(function(i, it) {
  965.  
  966. return new Promise((resolve, reject) => {
  967.  
  968. // 判断是否存在后缀
  969. if (it.indexOf(".") != -1) { // 存在
  970.  
  971. resolve(it);
  972.  
  973. } else { // 不存在
  974.  
  975. // 请求,不打开流,只需要头信息
  976. GM_xmlhttpRequest({
  977. method: 'GET',
  978. url: `http://${server}.sinaimg.cn/thumb150/${it}`,
  979. timeout: Config.maxRequestTime,
  980. responseType: "blob",
  981. onload: function(res) {
  982.  
  983. const postfix_regex = res.responseHeaders.match(/content-type: image\/(\w+)/);
  984.  
  985. // 找到,且图片类型为git
  986. if (postfix_regex && postfix_regex[1] == "gif") {
  987.  
  988. resolve(`${it}.gif`);
  989.  
  990. } else { // 未找到,或图片类型为:jpeg
  991.  
  992. resolve(`${it}.jpg`);
  993. }
  994. },
  995. onerror: function(e) {
  996.  
  997. console.error(e);
  998.  
  999. reject("请求图片格式发生错误!");
  1000. },
  1001. ontimeout: function() {
  1002.  
  1003. reject("请求图片格式超时!");
  1004. }
  1005. });
  1006. }
  1007. }).catch(e => {
  1008.  
  1009. Tip.error(e);
  1010.  
  1011. return `${it}.jpg`;
  1012. });
  1013. }).get());
  1014.  
  1015. // 去除重复
  1016. photo_ids_fix = Array.from(new Set(photo_ids_fix));
  1017.  
  1018. Core.log("总图片(fix):");
  1019. Core.log(photo_ids_fix);
  1020.  
  1021. return $(photo_ids_fix).map((i, it) => {
  1022.  
  1023. // 替换为大图链接
  1024. const src = `http://${server}.sinaimg.cn/large/${it}`;
  1025.  
  1026. Core.log(src);
  1027.  
  1028. const name = Core.getResourceName($ul, src, i, Config.mediaType.picture);
  1029.  
  1030. return new Link(name, src);
  1031. });
  1032. }
  1033. }
  1034. /*jshint esversion: 8 */
  1035.  
  1036. /**
  1037. * 视频处理器
  1038. */
  1039. class VideoHandler {
  1040.  
  1041. /**
  1042. * 处理视频如果需要
  1043. * @param {$标签对象} $ul 操作列表
  1044. */
  1045. static async handleVideoIfNeed($ul) {
  1046.  
  1047. const $button = Core.putButton($ul, "视频解析中...", null);
  1048.  
  1049. try {
  1050.  
  1051. const $box = Core.getWeiBoResolver().getVideoBox($ul);
  1052.  
  1053. // 不存在视频
  1054. if ($box.length === 0) {
  1055. return;
  1056. }
  1057.  
  1058. // 得到视频类型
  1059. const type = VideoHandler.getVideoType($box);
  1060.  
  1061.  
  1062. let $link;
  1063.  
  1064. if (type === "feedvideo") { // 短视屏(秒拍、梨视频、优酷)
  1065.  
  1066. $link = VideoHandler.getBlowVideoLink($box);
  1067.  
  1068. } else if (type === "feedlive") { // 直播回放
  1069.  
  1070. //TODO 暂不支持
  1071.  
  1072. } else if (type === "story") { // 微博故事
  1073.  
  1074. $link = VideoHandler.getWeiboStoryLink($box);
  1075.  
  1076. } else if (type === "adFeedVideo") { // 广告视频(无清晰度选择)
  1077.  
  1078.  
  1079. $link = VideoHandler.getAdVideoLink($box);
  1080.  
  1081. } else {
  1082.  
  1083. console.warn(`未知的类型:${type}`);
  1084. }
  1085.  
  1086. // 是否存在视频链接
  1087. if ($link) {
  1088.  
  1089. Core.handleCopy($ul, $([$link]));
  1090.  
  1091. const fun = () => VideoHandler.downloadVideo($box, $link);
  1092.  
  1093. Core.putButton($ul, "下载当前视频", fun);
  1094. }
  1095.  
  1096. } catch (e) {
  1097.  
  1098. console.error(e);
  1099.  
  1100. Tip.error(e.message);
  1101.  
  1102. Core.putButton($ul, "视频解析失败", null);
  1103.  
  1104. } finally {
  1105.  
  1106. Core.removeButton($ul, $button);
  1107. }
  1108. }
  1109.  
  1110. /**
  1111. * 得到视频类型
  1112. * @param {$标签对象} $box 视频容器
  1113. * @return {字符串} 视频类型[video、live]
  1114. */
  1115. static getVideoType($box) {
  1116.  
  1117. const typeRegex = $box.attr("action-data").match(/type=(\w+)&/);
  1118.  
  1119. return typeRegex[1];
  1120. }
  1121.  
  1122. /**
  1123. * 得到微博故事视频Link
  1124. *
  1125. * @param {$标签对象} $box 视频box
  1126. *
  1127. * @return {Link} 链接对象
  1128. */
  1129. static getWeiboStoryLink($box) {
  1130.  
  1131. const action_data = $box.attr("action-data");
  1132.  
  1133. const urlRegex = action_data.match(/gif_url=([\w%.]+)&/);
  1134.  
  1135. const url = urlRegex[1];
  1136.  
  1137. let src = decodeURIComponent(decodeURIComponent(url));
  1138.  
  1139. const name = Core.getResourceName($box, src.split("?")[0], 0, Config.mediaType.video);
  1140.  
  1141. if (src.indexOf("//") === 0) {
  1142. src = "https:" + src;
  1143. }
  1144.  
  1145. return new Link(name, src);
  1146. }
  1147.  
  1148. /**
  1149. * 得到广告视频Link
  1150. * @param {$标签对象} $box 视频box
  1151. *
  1152. * @return {Link} 链接对象
  1153. */
  1154. static getAdVideoLink($box) {
  1155.  
  1156. const src = SearchWeiBoResolver.geiVideoSrc($box);
  1157.  
  1158. name = Core.getResourceName($box, src.split("?")[0], 0, Config.mediaType.video);
  1159.  
  1160. Core.log(`download${name}=${src}`);
  1161.  
  1162. return new Link(name, src);
  1163. }
  1164.  
  1165. /**
  1166. * 得到酷燃视频Link
  1167. *
  1168. * @param {$标签对象} $box 视频box
  1169. *
  1170. * @return {Link} 链接对象
  1171. */
  1172. static getBlowVideoLink($box) {
  1173.  
  1174. let src, name;
  1175.  
  1176. try {
  1177.  
  1178. src = Core.getWeiBoResolver().geiVideoSrc($box);
  1179.  
  1180. if (!src) { // 未找到合适的视频地址
  1181.  
  1182. throw new Error("未能找到视频地址!");
  1183. }
  1184.  
  1185. name = Core.getResourceName($box, src.split("?")[0], 0, Config.mediaType.video);
  1186.  
  1187. Core.log(`download${name}=${src}`);
  1188.  
  1189. } catch (e) {
  1190.  
  1191. console.error(e);
  1192.  
  1193. throw new Error("未能找到视频地址!");
  1194. }
  1195.  
  1196. return new Link(name, src);
  1197. }
  1198.  
  1199. /**
  1200. * 下载直播回放
  1201. * @param {$标签对象} $li 视频box
  1202. */
  1203. static downloadLiveVCRVideo($ul, $li) {
  1204. // TODO 暂不支持
  1205. }
  1206.  
  1207. /**
  1208. * 下载视频
  1209. *
  1210. * @param {$标签对象} $box 视频box
  1211. * @param {$对象} $link Link对象
  1212. */
  1213. static downloadVideo($box, $link) {
  1214.  
  1215. Tip.info("即将开始下载...");
  1216.  
  1217. const progress = ZipHandler.bornProgress($box);
  1218.  
  1219. GM_download({
  1220. url: $link.src,
  1221. name: $link.name,
  1222. onprogress: function(p) {
  1223.  
  1224. const value = p.loaded / p.total;
  1225. progress.value = value;
  1226. },
  1227. onerror: function(e) {
  1228.  
  1229. console.error(e);
  1230.  
  1231. Tip.error("视频下载出错!");
  1232. }
  1233. });
  1234. }
  1235. }
  1236. /*jshint esversion: 6 */
  1237.  
  1238. class ZipHandler {
  1239.  
  1240. /**
  1241. * 生成一个进度条
  1242. * @param {$标签对象} $sub card的子节点
  1243. * @param {int} max 最大值
  1244. * @return {标签对象} 进度条
  1245. */
  1246. static bornProgress($sub) {
  1247.  
  1248. const $div = Core.getWeiBoResolver().getProgressContainer($sub);
  1249.  
  1250. // 尝试获取进度条
  1251. let $progress = $div.find('progress');
  1252.  
  1253. // 进度条不存在时,生成一个
  1254. if ($progress.length === 0) {
  1255.  
  1256. $progress = $("<progress max='1' style='margin-left:10px;' />");
  1257.  
  1258. $div.append($progress);
  1259.  
  1260. } else { // 已存在时,重置value
  1261.  
  1262. $progress[0].value = 0;
  1263. }
  1264.  
  1265. return $progress[0];
  1266. }
  1267.  
  1268. /**
  1269. * 开始打包
  1270. * @param {$数组} $links 图片地址集
  1271. */
  1272. static startZip($ul, $links) {
  1273.  
  1274. Tip.tip("正在提取,请稍候...", "iconExtract");
  1275.  
  1276. const progress = ZipHandler.bornProgress($ul);
  1277.  
  1278. const zip = new JSZip();
  1279.  
  1280. const names = [];
  1281.  
  1282. $links.each(function(i, it) {
  1283.  
  1284. const name = it.name;
  1285.  
  1286. GM_xmlhttpRequest({
  1287. method: 'GET',
  1288. url: it.src,
  1289. timeout: Config.maxRequestTime,
  1290. responseType: "blob",
  1291. onload: function(response) {
  1292.  
  1293. zip.file(name, response.response);
  1294.  
  1295. ZipHandler.downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
  1296. },
  1297. onerror: function(e) {
  1298.  
  1299. console.error(e);
  1300.  
  1301. Tip.error(`第${(i + 1)}个对象,获取失败!`);
  1302.  
  1303. ZipHandler.downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
  1304. },
  1305. ontimeout: function() {
  1306.  
  1307. Tip.error(`第${(i + 1)}个对象,请求超时!`);
  1308.  
  1309. ZipHandler.downloadZipIfComplete($ul, progress, name, zip, names, $links.length);
  1310. }
  1311. });
  1312. });
  1313. }
  1314.  
  1315. /**
  1316. * 下载打包,如果完成
  1317. */
  1318. static downloadZipIfComplete($ul, progress, name, zip, names, length) {
  1319.  
  1320. names.push(name);
  1321.  
  1322. const value = names.length / length;
  1323.  
  1324. progress.value = value;
  1325.  
  1326. if (names.length === length) {
  1327.  
  1328. Tip.tip("正在打包,请稍候...", "iconZip");
  1329.  
  1330. zip.generateAsync({
  1331. type: "blob"
  1332. }, function(metadata) {
  1333.  
  1334. progress.value = metadata.percent / 100;
  1335.  
  1336. }).then(function(content) {
  1337.  
  1338. Tip.success("打包完成,即将开始下载!");
  1339.  
  1340. const zipName = Core.getZipName($ul);
  1341.  
  1342. saveAs(content, `${zipName}.zip`);
  1343. });
  1344. }
  1345. }
  1346. }
  1347. /*jshint esversion: 6 */
  1348.  
  1349. /**
  1350. * 提示
  1351. */
  1352. class Tip {
  1353.  
  1354. static tip(text, iconName) {
  1355.  
  1356. if (Config.isTip) {
  1357.  
  1358. GM_notification({
  1359. text: text,
  1360. image: GM_getResourceURL(iconName),
  1361. timeout: 3000,
  1362. });
  1363. }
  1364. }
  1365.  
  1366. static info(text) {
  1367. Tip.tip(text, "iconInfo");
  1368. }
  1369.  
  1370. static error(text) {
  1371. Tip.tip(text, "iconError");
  1372. }
  1373.  
  1374. static success(text) {
  1375. Tip.tip(text, "iconSuccess");
  1376. }
  1377. }
  1378. /*jshint esversion: 8 */
  1379.  
  1380. /**
  1381. * 核心
  1382. */
  1383. class Core {
  1384.  
  1385. /**
  1386. * 处理微博卡片
  1387. */
  1388. static handleWeiBoCard() {
  1389.  
  1390. // 查找未被扩展的操作按钮
  1391. const $operationButtons = Core.getWeiBoResolver().getOperationButton();
  1392.  
  1393. // 存在未被扩展的操作按钮
  1394. if ($operationButtons.length > 0) {
  1395.  
  1396. console.info(`找到未被扩展的操作按钮:${$operationButtons.length}`);
  1397.  
  1398. $operationButtons.one("click", event =>
  1399. Core.resolveWeiBoCard($(event.currentTarget))
  1400. );
  1401.  
  1402. $operationButtons.addClass(Config.handledWeiBoCardClass);
  1403. }
  1404. }
  1405.  
  1406. /**
  1407. * 解析 微博卡片
  1408. * 仅在初次点击 操作按钮[↓] 时,触发
  1409. *
  1410. * @param {$标签对象} $operationButton 操作按钮
  1411. */
  1412. static resolveWeiBoCard($operationButton) {
  1413.  
  1414. const weiboResolver = Core.getWeiBoResolver();
  1415.  
  1416. const $ul = weiboResolver.getOperationList($operationButton);
  1417.  
  1418. PictureHandler.handlePictureIfNeed($ul);
  1419. VideoHandler.handleVideoIfNeed($ul);
  1420. }
  1421.  
  1422. /**
  1423. * 得到微博解析器
  1424. */
  1425. static getWeiBoResolver() {
  1426.  
  1427. let resolver;
  1428.  
  1429. // 微博搜索
  1430. if (window.location.href.indexOf("https://s.weibo.com") === 0) {
  1431.  
  1432. resolver = SearchWeiBoResolver;
  1433.  
  1434. } else { // 我的微博、他人微博、我的收藏、热门微博
  1435.  
  1436. resolver = MyWeiBoResolver;
  1437. }
  1438.  
  1439. return resolver;
  1440. }
  1441.  
  1442. /**
  1443. * 添加按钮
  1444. * @param {$标签对象} $ul 操作列表
  1445. * @param {字符串} name 按钮名称
  1446. * @param {方法} op 按钮操作
  1447. *
  1448. * @return {$控件} 按钮
  1449. */
  1450. static putButton($ul, name, op) {
  1451.  
  1452. const $li = $(`<li><a href='javascript:void(0)'>—> ${name} <—</a></li>`);
  1453.  
  1454. $li.click(op);
  1455.  
  1456. $ul.append($li);
  1457.  
  1458. return $li;
  1459. }
  1460.  
  1461. /**
  1462. * 移除按钮
  1463. * @param {$标签对象} $ul 操作列表
  1464. * @param {$控件} $button 按钮
  1465. */
  1466. static removeButton($ul, $button) {
  1467.  
  1468. $ul.find(`li a:contains(${$button.text()})`).remove();
  1469. }
  1470.  
  1471. /**
  1472. * 处理拷贝
  1473. *
  1474. * @param {$对象} $ul 操作列表
  1475. * @param {$数组} $links Link数组
  1476. */
  1477. static handleCopy($ul, $links) {
  1478.  
  1479. Core.putButton($ul, "复制资源链接", function() {
  1480.  
  1481. const link = $links.get().map(function(it, i) {
  1482. return it.src;
  1483. }).join("\n");
  1484.  
  1485. GM_setClipboard(link, "text");
  1486.  
  1487. Tip.success("链接地址已复制到剪贴板!");
  1488. });
  1489. }
  1490.  
  1491. /**
  1492. * 得到打包名称
  1493. *
  1494. * @param {$标签对象} $ul 操作列表
  1495. * @return {字符串} 压缩包名称(不含后缀)
  1496. */
  1497. static getZipName($ul) {
  1498.  
  1499. const weiBoResolver = Core.getWeiBoResolver();
  1500.  
  1501.  
  1502. const $info = weiBoResolver.getWeiBoInfo($ul);
  1503. const wb_id = weiBoResolver.getWeiBoId($ul, $info, false);
  1504. const wb_user_id = weiBoResolver.getWeiBoUserId($ul, $info, false);
  1505. const wb_user_name = weiBoResolver.getWeiBoUserName($ul, $info, false);
  1506. const wb_url = weiBoResolver.getWeiBoUrl($ul, false);
  1507.  
  1508. const $root_info = weiBoResolver.getRootWeiBoInfo($ul);
  1509. const wb_root_id = weiBoResolver.getWeiBoId($ul, $root_info, true);
  1510. const wb_root_user_id = weiBoResolver.getWeiBoUserId($ul, $root_info, true);
  1511. const wb_root_user_name = weiBoResolver.getWeiBoUserName($ul, $root_info, true);
  1512. const wb_root_url = weiBoResolver.getWeiBoUrl($ul, true);
  1513.  
  1514. const name = Config.getZipName(
  1515. wb_user_name, wb_user_id,
  1516. wb_id, wb_url,
  1517. wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id
  1518. );
  1519.  
  1520. return name;
  1521. }
  1522.  
  1523. /**
  1524. * 得到资源原始名称(不含后缀)
  1525. * @param {字符串} path 路径
  1526. * @return {字符串} 名称(不含后缀)
  1527. */
  1528. static getPathName(path) {
  1529.  
  1530. const start = path.lastIndexOf("/") + 1;
  1531. const end = path.lastIndexOf(".");
  1532.  
  1533. let name;
  1534.  
  1535. if (end > start) {
  1536.  
  1537. name = path.substring(start, end);
  1538.  
  1539. } else {
  1540.  
  1541. name = path.substring(start);
  1542. }
  1543.  
  1544. Core.log(`截得名称为:${name}`);
  1545.  
  1546. return name;
  1547. }
  1548.  
  1549. /**
  1550. * 得到后缀
  1551. * @param {字符串} path 路径
  1552. * @param {字符串} media_type 媒体类型
  1553. *
  1554. * @return {字符串} 后缀(含.)
  1555. */
  1556. static getPathPostfix(path, media_type) {
  1557.  
  1558. let postfix = path.substring(path.lastIndexOf(".") + 1).toLowerCase();
  1559.  
  1560. Core.log(`截得后缀为:${postfix}`);
  1561.  
  1562. // 媒体类型为图片
  1563. if (media_type == Config.mediaType.picture) {
  1564.  
  1565. const pics = ["jpg", "jpeg", "gif", "png", "bmp", "tif"];
  1566.  
  1567. // 此格式的后缀不是一个常见格式,可能是解析错误导致
  1568. // 也可能就是一个冷门格式,但此格式若使用GM进行下载,则会受到限制
  1569. if ($.inArray(postfix, pics) == -1) {
  1570.  
  1571. console.warn(`不能识别的【${media_type}】格式:${postfix},Ta即将被覆盖为${pics[0]}。`);
  1572.  
  1573. postfix = pics[0];
  1574.  
  1575. }
  1576.  
  1577. } else if (media_type == Config.mediaType.video ||
  1578. media_type == Config.mediaType.livePhoto) { // 媒体类型为视频
  1579.  
  1580. const vids = ["mp4", "wmv", "avi", "ts", "mov"];
  1581.  
  1582. if ($.inArray(postfix, vids) == -1) {
  1583.  
  1584. console.warn(`不能识别的【${media_type}】格式:${postfix},Ta即将被覆盖为${vids[0]}。`);
  1585.  
  1586. postfix = vids[0];
  1587. }
  1588. }
  1589.  
  1590. return `.${postfix}`;
  1591. }
  1592.  
  1593.  
  1594. /**
  1595. * 得到资源名称
  1596. *
  1597. * @param {$标签对象} $ul 操作列表
  1598. * @param {字符串} src 资源地址
  1599. * @param {整数} index 序号
  1600. * @param {字符串} media_type 媒体类型
  1601. *
  1602. * @return {字符串} 资源名称(含后缀)
  1603. */
  1604. static getResourceName($ul, src, index, media_type) {
  1605.  
  1606. const weiBoResolver = Core.getWeiBoResolver();
  1607.  
  1608. const resource_id = Core.getPathName(src);
  1609.  
  1610.  
  1611. const $info = weiBoResolver.getWeiBoInfo($ul);
  1612. const wb_id = weiBoResolver.getWeiBoId($ul, $info, false);
  1613. const wb_user_id = weiBoResolver.getWeiBoUserId($ul, $info, false);
  1614. const wb_user_name = weiBoResolver.getWeiBoUserName($ul, $info, false);
  1615. const wb_url = weiBoResolver.getWeiBoUrl($ul, false);
  1616.  
  1617. const $root_info = weiBoResolver.getRootWeiBoInfo($ul);
  1618. const wb_root_id = weiBoResolver.getWeiBoId($ul, $root_info, true);
  1619. const wb_root_user_id = weiBoResolver.getWeiBoUserId($ul, $root_info, true);
  1620. const wb_root_user_name = weiBoResolver.getWeiBoUserName($ul, $root_info, true);
  1621. const wb_root_url = weiBoResolver.getWeiBoUrl($ul, true);
  1622.  
  1623. // 修正,从1开始
  1624. index++;
  1625.  
  1626. // 补齐位数:01、02、03...
  1627. if (index.toString().length === 1) {
  1628. index = "0" + index.toString();
  1629. }
  1630.  
  1631. const no = index;
  1632.  
  1633. const postfix = Core.getPathPostfix(src, media_type);
  1634.  
  1635. const name = Config.getResourceName(
  1636. wb_user_name, wb_user_id, wb_id, wb_url,
  1637. resource_id, no, media_type,
  1638. wb_root_user_name, wb_root_user_id, wb_root_url, wb_root_id) + postfix;
  1639.  
  1640. return name;
  1641. }
  1642.  
  1643. /**
  1644. * 记录日志
  1645. * @param {字符串} msg 日志内容
  1646. */
  1647. static log(msg) {
  1648. if (Config.isDebug) {
  1649. console.log(msg);
  1650. }
  1651. }
  1652.  
  1653. /**
  1654. * 得到大图服务器
  1655. *
  1656. * @param {$控件} $ul 操作列表
  1657. * @return {字符串} 服务器
  1658. */
  1659. static getLargeImageServer($ul) {
  1660.  
  1661. const weiBoResolver = Core.getWeiBoResolver();
  1662.  
  1663. const $imgs = weiBoResolver.get9PhotoImgs($ul);
  1664.  
  1665. const src = $($imgs[0]).attr("src");
  1666.  
  1667. let server;
  1668.  
  1669. if (src) {
  1670.  
  1671. const server_regex = src.match(/(wx\d)\.sinaimg\.cn/);
  1672.  
  1673. if (server_regex) {
  1674.  
  1675. server = server_regex[1];
  1676. }
  1677. }
  1678.  
  1679. if (!server) {
  1680.  
  1681. // 缺省服务器
  1682. server = "wx2";
  1683. }
  1684.  
  1685. return server;
  1686. }
  1687. }
  1688. setInterval(Core.handleWeiBoCard, Config.space);
  1689. })();