下载Bilibili动态页面图片

下载“Bilibili动态”时间线页面的图片,也可下载视频(720P单文件)

  1. // ==UserScript==
  2. // @name Bilibili Download Pictures
  3. // @name:zh-CN 下载Bilibili动态页面图片
  4. // @version 0.9.9
  5. // @description Download pictures from bilibili timeline and 720P videos.
  6. // @description:zh-CN 下载“Bilibili动态”时间线页面的图片,也可下载视频(720P单文件)
  7. // @author OWENDSWANG
  8. // @icon https://avatars.githubusercontent.com/u/9076865?s=40&v=4
  9. // @license MIT
  10. // @homepage https://gf.qytechs.cn/scripts/421885
  11. // @supportURL https://github.com/owendswang/Download-Pictures-from-Bilibili-Timeline/issues
  12. // @match https://t.bilibili.com/*
  13. // @match https://space.bilibili.com/*/dynamic*
  14. // @match https://www.bilibili.com/opus/*
  15. // @match https://www.bilibili.com/video/*
  16. // @match https://www.bilibili.com/v/topic/detail/?*
  17. // @connect bilibili.com
  18. // @connect bilivideo.com
  19. // @connect bilivideo.cn
  20. // @connect hdslb.com
  21. // @connect biliimg.com
  22. // @grant GM_download
  23. // @grant GM_xmlhttpRequest
  24. // @grant GM_getValue
  25. // @grant GM_setValue
  26. // @grant GM_cookie
  27. // @grant GM_registerMenuCommand
  28. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
  29. // @namespace https://gf.qytechs.cn/users/738244
  30. // ==/UserScript==
  31.  
  32. (function() {
  33. 'use strict';
  34.  
  35. // Your code here...
  36. const settingVersion = 1;
  37. const downloadIcon = 'url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAHpSURBVDiNndS7a1RREMfxz9XVrI8UIkjwLxCipYVp9A+wEQsV8dFksLCwUATxAYqNIAhWO4UPsAqCCGqTQkRQULEMgojYCSK+X4nhWty7uCZ3k5gfnObMOd8zM2dmirIs9SozV+IxNmCJZk3jObZGxFSvodVweD024TMm+wDb2Iy1eDsfcCkKHMSdPsB9uFKf/UdNwK4mI+J3kyEzp5r26Z8jTa8v5N5cwEWpCdgYZh/NOtuCzGyrQiwx+B/Awcz8ovrE6Yj42crMcYyoSqRdr/nULd6X+IFfWJaZYy1cwhA24j5u1t4+mAN4D0cwhb21Q89wtSjLUmauxm1sw56IGFtIvJm5H9dxFzsj4lfR23qZeQ0HcDEijs4Du4zD6ETEoe5+0el0zuNnURTnRkdHZeYpnMUt7JrZq5k5oErLdhyPiAuZuQyn8XEJTuBMWZbLISLOqVprB55k5roe2BCe1rDdEXGhNq3CSRxrqX73U68XEXEjM99gHBOZuUVVs4+wAiMR8bjnSolv+NC3UyLiIYZVU+cFJvARwzNg/6gLLDVUfUS8UpXTw3ptjIjXDZzpmlF2p02BdmbOmn8R8V1VTiAzmybUQM0oik6n81WVl/f9wvC3M4o+9kI1bD+08A5r6rVYlapcf/0DW06ifC1dVCUAAAAASUVORK5CYII=\')';
  38. let notLoaded = true;
  39. let cardsTotal = 0;
  40. let skeletonsTotal = 0;
  41. let downloadQueueCard = document.createElement('div');
  42. downloadQueueCard.style.position = 'fixed';
  43. downloadQueueCard.style.bottom = '0.5rem';
  44. downloadQueueCard.style.left = '0.5rem';
  45. downloadQueueCard.style.maxHeight = '50vh';
  46. downloadQueueCard.style.overflowY = 'auto';
  47. downloadQueueCard.style.overflowX = 'hidden';
  48. downloadQueueCard.style.zIndex = '10';
  49. let downloadQueueTitle = document.createElement('div');
  50. downloadQueueTitle.textContent = '下载队列';
  51. downloadQueueTitle.style.fontSize = '0.8rem';
  52. downloadQueueTitle.style.color = 'gray';
  53. downloadQueueTitle.style.display = 'none';
  54. downloadQueueCard.appendChild(downloadQueueTitle);
  55. document.body.appendChild(downloadQueueCard);
  56. let progressBar = document.createElement('div');
  57. progressBar.style.height = '1.4rem';
  58. progressBar.style.width = '17rem';
  59. // progressBar.style.background = 'linear-gradient(to right, red 100%, transparent 100%)';
  60. progressBar.style.borderStyle = 'solid';
  61. progressBar.style.borderWidth = '0.1rem';
  62. progressBar.style.borderColor = 'grey';
  63. progressBar.style.borderRadius = '0.5rem';
  64. progressBar.style.boxSizing = 'content-box';
  65. progressBar.style.marginTop = '0.5rem';
  66. progressBar.style.marginRight = '1rem';
  67. progressBar.style.position = 'relative';
  68. let progressText = document.createElement('div');
  69. // progressText.textContent = 'test.test';
  70. progressText.style.mixBlendMode = 'screen';
  71. progressText.style.width = '100%';
  72. progressText.style.textAlign = 'center';
  73. progressText.style.color = 'orange';
  74. progressText.style.fontSize = '0.7rem';
  75. progressText.style.lineHeight = '1.4rem';
  76. progressText.style.overflow = 'hidden';
  77. progressBar.appendChild(progressText);
  78. let progressCloseBtn = document.createElement('button');
  79. progressCloseBtn.style.border = 'unset';
  80. progressCloseBtn.style.background = 'unset';
  81. progressCloseBtn.style.color = 'orange';
  82. progressCloseBtn.style.position = 'absolute';
  83. progressCloseBtn.style.right = '0.3rem';
  84. progressCloseBtn.style.top = '0.2rem';
  85. progressCloseBtn.style.fontSize = '1rem';
  86. progressCloseBtn.style.lineHeight = '1rem';
  87. progressCloseBtn.style.cursor = 'pointer';
  88. progressCloseBtn.textContent = '×';
  89. progressCloseBtn.title = '取消';
  90. progressCloseBtn.onmouseover = function(e){
  91. this.style.color = 'red';
  92. }
  93. progressCloseBtn.onmouseout = function(e){
  94. this.style.color = 'orange';
  95. }
  96. progressBar.appendChild(progressCloseBtn);
  97. // downloadQueueCard.appendChild(progressBar);
  98. function oXMLHttpRequest(url, type) {
  99. return new Promise(function(resolve, reject) {
  100. let oReq = new XMLHttpRequest();
  101. oReq.open("GET", url);
  102. oReq.withCredentials = true;
  103. oReq.responseType = type;
  104. oReq.onload = (e) => {
  105. // console.log(e);
  106. // console.log(oReq.response);
  107. resolve(oReq.response);
  108. };
  109. oReq.onerror = (e) => { console.log(e); alert('请求失败!'); resolve(null); };
  110. oReq.onabort = (e) => { console.log(e); alert('请求被中断!'); resolve(null); };
  111. oReq.ontimeout = (e) => { console.log(e); alert('请求超时!'); resolve(null); };
  112. oReq.send(null);
  113. });
  114. }
  115. /*function saveAs(blob, name) {
  116. const link = document.createElement("a");
  117. link.style.display = "none";
  118. link.href = URL.createObjectURL(blob);
  119. link.download = name;
  120. link.target = '_blank';
  121. document.body.appendChild(link);
  122. // console.log(link);
  123. link.click();
  124. const timeout = setTimeout(() => {
  125. URL.revokeObjectURL(link.href);
  126. link.parentNode.removeChild(link);
  127. }, 1000);
  128. }*/
  129. function downloadError(e, url, name, progress) {
  130. // console.log(e, url);
  131. /*GM_notification({
  132. title: 'Download error',
  133. text: 'Error: ' + e.error + '\nUrl: ' + url,
  134. silent: true,
  135. timeout: 3,
  136. });*/
  137. progress.style.background = 'red';
  138. progress.firstChild.textContent = (name.length > 10 ? (name.substring(0,10) + '...') : name) + ' [' + (e.error || 'Unknown') + ']';
  139. progress.firstChild.style.color = 'yellow';
  140. progress.firstChild.style.mixBlendMode = 'unset';
  141. let progressRetryBtn = document.createElement('button');
  142. progressRetryBtn.style.border = 'unset';
  143. progressRetryBtn.style.background = 'unset';
  144. progressRetryBtn.style.color = 'yellow';
  145. progressRetryBtn.style.position = 'absolute';
  146. progressRetryBtn.style.right = '1.2rem';
  147. progressRetryBtn.style.top = '0.05rem';
  148. progressRetryBtn.style.fontSize = '1rem';
  149. progressRetryBtn.style.lineHeight = '1rem';
  150. progressRetryBtn.style.cursor = 'pointer';
  151. progressRetryBtn.style.letterSpacing = '-0.2rem';
  152. progressRetryBtn.textContent = '⤤⤦';
  153. progressRetryBtn.title = '重试';
  154. progressRetryBtn.onmouseover = function(e){
  155. this.style.color = 'white';
  156. }
  157. progressRetryBtn.onmouseout = function(e){
  158. this.style.color = 'yellow';
  159. }
  160. progressRetryBtn.onclick = function(e) {
  161. this.parentNode.remove();
  162. downloadWrapper(url, name);
  163. }
  164. progress.insertBefore(progressRetryBtn, progress.lastChild);
  165. progress.lastChild.title = '关闭';
  166. progress.lastChild.style.color = 'yellow';
  167. progress.lastChild.onmouseover = function(e){
  168. this.style.color = 'white';
  169. };
  170. progress.lastChild.onmouseout = function(e){
  171. this.style.color = 'yellow';
  172. };
  173. progress.lastChild.onclick = function(e) {
  174. this.parentNode.remove();
  175. if(progress.parent.childElementCount == 1) progress.parent.firstChild.style.display = 'none';
  176. };
  177. // setTimeout(() => { progress.remove(); if(downloadQueueCard.childElementCount == 1) downloadQueueTitle.style.display = 'none'; }, 1000);
  178. }
  179. function downloadWrapper(url, name) {
  180. // console.log('downloadWrapper: ', url, name);
  181. downloadQueueTitle.style.display = 'block';
  182. let progress = downloadQueueCard.appendChild(progressBar.cloneNode(true));
  183. progress.firstChild.textContent = (name.length > 10 ? (name.substring(0,10) + '...') : name) + ' [0%]';
  184. return new Promise(function(resolve, reject) {
  185. const download = GM_xmlhttpRequest({
  186. method: 'GET',
  187. url,
  188. responseType: 'blob',
  189. headers: {
  190. Referer: location.protocol + '//' + location.hostname,
  191. Origin: location.protocol + '//' + location.hostname,
  192. },
  193. onprogress: (e) => {
  194. // e = { int done, finalUrl, bool lengthComputable, int loaded, int position, int readyState, response, str responseHeaders, responseText, responseXML, int status, statusText, int total, int totalSize }
  195. const percent = e.done / e.total * 100;
  196. progress.style.background = 'linear-gradient(to right, green ' + percent + '%, transparent ' + percent + '%)';
  197. progress.firstChild.textContent = (name.length > 10 ? (name.substring(0,10) + '...') : name) + ' [' + percent.toFixed(0) + '%]';
  198. },
  199. onload: function({ response }) {
  200. // console.log(response);
  201. const timeout = setTimeout(() => {
  202. progress.remove();
  203. if(downloadQueueCard.childElementCount == 1) downloadQueueTitle.style.display = 'none';
  204. }, 1000);
  205. progress.lastChild.onclick = function(e) {
  206. clearTimeout(timeout);
  207. this.parentNode.remove();
  208. if(downloadQueueCard.childElementCount == 1) downloadQueueTitle.style.display = 'none';
  209. };
  210. saveAs(response, name);
  211. resolve(null);
  212. },
  213. onabort: function(e) { console.log(e); resolve(null); },
  214. onerror: function(e) { downloadError(e, url, name, progress); console.log(e); resolve(null); },
  215. ontimeout: function(e) { downloadError(e, url, name, progress); console.log(e); resolve(null); },
  216. });
  217. progress.lastChild.onclick = function(e) {
  218. download.abort();
  219. this.parentNode.remove();
  220. if(downloadQueueCard.childElementCount == 1) downloadQueueTitle.style.display = 'none';
  221. };
  222. });
  223. }
  224. /*function getCookie(name) {
  225. return new Promise(function(resolve, reject) {
  226. GM_cookie.list({ name: name }, function(cookies, error) {
  227. if (!error) {
  228. // console.log(cookies);
  229. resolve(cookies[0].value);
  230. } else {
  231. console.error(error);
  232. }
  233. });
  234. });
  235. }
  236. function getAllCookies() {
  237. return new Promise(function(resolve, reject) {
  238. GM_cookie.list({}, function(cookies, error) {
  239. if (!error) {
  240. // console.log(cookies);
  241. const cookiesStr = cookies.map((ele) => { return ele.name + '=' + ele.value }).join('; ');
  242. // console.log(cookiesStr);
  243. resolve(cookiesStr);
  244. } else {
  245. console.error(error);
  246. resolve(null);
  247. }
  248. });
  249. });
  250. }
  251. function download2Blob(url) {
  252. // console.log('download2Blob: ', url);
  253. return new Promise(function(resolve, reject) {
  254. GM_xmlhttpRequest({
  255. method: 'GET',
  256. url,
  257. responseType: 'blob',
  258. headers: {
  259. Referer: location.protocol + '//' + location.hostname,
  260. Origin: location.protocol + '//' + location.hostname,
  261. },
  262. onload: function({ response }) {
  263. // console.log(response);
  264. // saveAs(response, name);
  265. resolve(response);
  266. },
  267. onabort: function(e) { console.log(e); alert('请求被中断!'); resolve(null); },
  268. onerror: function(e) { console.log(e); alert('请求失败!'); resolve(null); },
  269. ontimeout: function(e) { console.log(e); alert('下载超时!'); resolve(null); },
  270. });
  271. });
  272. }*/
  273. function getPicName(nameSetting, originalName, index, data) {
  274. const card = JSON.parse(data.card.card);
  275. let setName = nameSetting;
  276. setName = setName.replace('{original}', originalName.split('.')[0]);
  277. setName = setName.replace('{ext}', originalName.split('.')[1]);
  278. const userName = card.user?.name || data.card.desc.user_profile.info.uname;
  279. const userId = card.user?.uid || data.card.desc.user_profile.info.uid;
  280. const dynamicId = data.card.desc.dynamic_id;
  281. const content = card.item?.description || card.title;
  282. setName = setName.replace('{username}', userName);
  283. setName = setName.replace('{userid}', userId);
  284. setName = setName.replace('{dynamicid}', dynamicId);
  285. setName = setName.replace('{index}', index);
  286. setName = setName.replace('{content}', content.substring(0, 25));
  287. let YYYY, MM, DD, HH, mm, ss;
  288. const postAt = new Date((card.item?.upload_time || data.card.desc.timestamp) * 1000);
  289. YYYY = postAt.getFullYear().toString();
  290. MM = (postAt.getMonth() + 1).toString().padStart(2, '0');
  291. DD = postAt.getDate().toString().padStart(2, '0');
  292. HH = postAt.getHours().toString().padStart(2, '0');
  293. mm = postAt.getMinutes().toString().padStart(2, '0');
  294. ss = postAt.getSeconds().toString().padStart(2, '0');
  295. setName = setName.replace('{YYYY}', YYYY);
  296. setName = setName.replace('{MM}', MM);
  297. setName = setName.replace('{DD}', DD);
  298. setName = setName.replace('{HH}', HH);
  299. setName = setName.replace('{mm}', mm);
  300. setName = setName.replace('{ss}', ss);
  301. /*if (retweetPostId && GM_getValue('retweetMode', false)) {
  302. setName = setName.replace('{re.mblogid}', retweetPostId);
  303. setName = setName.replace('{re.username}', retweetUserName);
  304. setName = setName.replace('{re.userid}', retweetUserId);
  305. setName = setName.replace('{re.uid}', retweetPostUid);
  306. setName = setName.replace('{re.content}', retweetContent.substring(0, 25));
  307. let reYYYY, reMM, reDD, reHH, remm, ress;
  308. const retweetPostAt = new Date(retweetPostTime);
  309. if (retweetPostTime) {
  310. reYYYY = retweetPostAt.getFullYear().toString();
  311. reMM = (retweetPostAt.getMonth() + 1).toString().padStart(2, '0');
  312. reDD = retweetPostAt.getDate().toString().padStart(2, '0');
  313. reHH = retweetPostAt.getHours().toString().padStart(2, '0');
  314. remm = retweetPostAt.getMinutes().toString().padStart(2, '0');
  315. ress = retweetPostAt.getSeconds().toString().padStart(2, '0');
  316. }
  317. setName = setName.replace('{re.YYYY}', reYYYY);
  318. setName = setName.replace('{re.MM}', reMM);
  319. setName = setName.replace('{re.DD}', reDD);
  320. setName = setName.replace('{re.HH}', reHH);
  321. setName = setName.replace('{re.mm}', remm);
  322. setName = setName.replace('{re.ss}', ress);
  323. }*/
  324. return setName.replace(/[<|>|*|"|\/|\\|\||:|?|\n]/g, '_');
  325. }
  326. function getVidName(nameSetting, originalName, data) {
  327. let setName = nameSetting;
  328. setName = setName.replace('{original}', originalName.split('.')[0]);
  329. setName = setName.replace('{ext}', originalName.split('.')[1]);
  330. const bvid = data.bvid;
  331. const aid = data.aid;
  332. const cid = data.cid;
  333. const title = data.title;
  334. const content = data.desc;
  335. const userName = data.owner.name;
  336. const userId = data.owner.mid;
  337. setName = setName.replace('{bvid}', bvid);
  338. setName = setName.replace('{aid}', aid);
  339. setName = setName.replace('{cid}', cid);
  340. setName = setName.replace('{title}', title);
  341. setName = setName.replace('{content}', content.substring(0, 25));
  342. setName = setName.replace('{username}', userName);
  343. setName = setName.replace('{userid}', userId);
  344. let YYYY, MM, DD, HH, mm, ss;
  345. const postAt = new Date(data.ctime * 1000);
  346. YYYY = postAt.getFullYear().toString();
  347. MM = (postAt.getMonth() + 1).toString().padStart(2, '0');
  348. DD = postAt.getDate().toString().padStart(2, '0');
  349. HH = postAt.getHours().toString().padStart(2, '0');
  350. mm = postAt.getMinutes().toString().padStart(2, '0');
  351. ss = postAt.getSeconds().toString().padStart(2, '0');
  352. setName = setName.replace('{YYYY}', YYYY);
  353. setName = setName.replace('{MM}', MM);
  354. setName = setName.replace('{DD}', DD);
  355. setName = setName.replace('{HH}', HH);
  356. setName = setName.replace('{mm}', mm);
  357. setName = setName.replace('{ss}', ss);
  358. return setName.replace(/[<|>|*|"|\/|\\|\||:|?|\n]/g, '_');
  359. }
  360. function handleImageDynamic(data) {
  361. const card = JSON.parse(data.card.card);
  362. const pictures = card.item.pictures;
  363. // console.log(pictures);
  364. for (const [ index, picture ] of pictures.entries()) {
  365. // console.log(picture);
  366. const pictureUrl = picture.img_src;
  367. const originalName = pictureUrl.split('/')[pictureUrl.split('/').length - 1];
  368. const pictureName = getPicName(GM_getValue('dlPicName', '{original}.{ext}'), originalName, index + 1, data);
  369. /*GM_download({
  370. url: pictureUrl,
  371. name: pictureName,
  372. onerror: function(e) { console.log(e); alert('下载失败!'); },
  373. ontimeout: function(e) { console.log(e); alert('下载超时!'); },
  374. });*/
  375. downloadWrapper(pictureUrl, pictureName);
  376. }
  377. }
  378. async function handleArticleDynamic(data) {
  379. const card = JSON.parse(data.card.card);
  380. const pictures = card.image_urls;
  381. // console.log(pictures);
  382. for (const [ index, picture ] of pictures.entries()) {
  383. // console.log(picture);
  384. const pictureUrl = picture;
  385. const originalName = pictureUrl.split('/')[pictureUrl.split('/').length - 1];
  386. const pictureName = getPicName(GM_getValue('dlPicName', '{original}.{ext}'), originalName, index + 1, data);
  387. /*GM_download({
  388. url: pictureUrl,
  389. name: pictureName,
  390. onerror: function(e) { console.log(e); alert('下载失败!'); },
  391. ontimeout: function(e) { console.log(e); alert('下载超时!'); },
  392. });*/
  393. downloadWrapper(pictureUrl, pictureName);
  394. }
  395. }
  396. function getVideoInfo(bvid) {
  397. // console.log('getVideoInfo');
  398. return oXMLHttpRequest('https://api.bilibili.com/x/web-interface/view?bvid=' + bvid, 'json');
  399. }
  400. function getVideoDetail(aid, cid/*, cookies*/) {
  401. /*return new Promise(function(resolve, reject) {
  402. // console.log(aid, cid, cookies);
  403. GM_xmlhttpRequest({
  404. method: 'GET',
  405. // 1080p dash --> https://api.bilibili.com/x/player/playurl?avid=1551880723&cid=1473551215&qn=80&fnval=4048
  406. // 720p mp4 --> https://api.bilibili.com/x/player/playurl?avid=1551880723&cid=1473551215&qn=64
  407. url: 'https://api.bilibili.com/x/player/wbi/playurl?avid=' + aid.toString() + '&cid=' + cid.toString() + '&fnval=64',
  408. responseType: 'json',
  409. anonymous: true,
  410. cookie: cookies,
  411. onload: function({ response }) {
  412. // console.log(response);
  413. resolve(response);
  414. },
  415. onabort: function(e) { resolve(null); },
  416. onerror: function(e) { resolve(null); },
  417. ontimeout: function(e) { resolve(null); },
  418. });
  419. });*/
  420. return oXMLHttpRequest('https://api.bilibili.com/x/player/wbi/playurl?avid=' + aid.toString() + '&cid=' + cid.toString() + '&fnval=64', 'json');
  421. }
  422. async function downloadVideo(data) {
  423. const vidRes = await getVideoDetail(data.aid, data.cid/*, cookies*/);
  424. // console.log(vidRes);
  425. const vidUrl = vidRes.data.durl[0].url;
  426. // console.log(vidUrl);
  427. const originalName = vidUrl.split('?')[0].split('/')[vidUrl.split('?')[0].split('/').length - 1];
  428. const vidName = getVidName(GM_getValue('dlVidName', '{original}.{ext}'), originalName, data);
  429. // console.log(vidName);
  430. /*GM_download({
  431. url: vidUrl,
  432. name: vidName,
  433. headers: {
  434. Referer: 'https://www.bilibili.com',
  435. Origin: 'https://www.bilibili.com',
  436. },
  437. onerror: function(e) { console.log(e); },
  438. ontimeout: function(e) { console.log(e); },
  439. });
  440. const blob = await download2Blob(vidUrl);
  441. const url = URL.createObjectURL(blob);
  442. // console.log(url);
  443. GM_download({
  444. url: url,
  445. name: vidName,
  446. onerror: function(e) { console.log(e); alert('下载失败!'); },
  447. ontimeout: function(e) { console.log(e); alert('下载超时!'); },
  448. });
  449. saveAs(blob, vidName);*/
  450. downloadWrapper(vidUrl, vidName);
  451. }
  452. async function handleVideoDownload(bvid) {
  453. const vidInfoRes = await getVideoInfo(bvid);
  454. // console.log(vidInfoRes);
  455. await downloadVideo(vidInfoRes.data);
  456. }
  457. async function handleVideoDynamic(data) {
  458. // console.log('handleVideoDynamic');
  459. // console.log(data);
  460. // const card = JSON.parse(data.card.card);
  461. // const aid = card.aid;
  462. // const cid = card.cid;
  463. // console.log(aid, cid);
  464. // const cookies = await getAllCookies();
  465. // const cookies = 'SESSDATA=' + await getCookie('SESSDATA');
  466. // console.log(cookies);
  467. // await downloadVideo(aid, cid);
  468. const bvid = data.card.desc.bvid;
  469. await handleVideoDownload(bvid);
  470. }
  471. function getDynamicDetail(dynId) {
  472. /*return new Promise(function(resolve, reject) {
  473. GM_xmlhttpRequest({
  474. method: 'GET',
  475. url: 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id=' + dynId,
  476. responseType: 'json',
  477. onload: function({ response }) {
  478. // console.log(response);
  479. resolve(response);
  480. },
  481. onabort: function(e) { resolve(null); },
  482. onerror: function(e) { resolve(null); },
  483. ontimeout: function(e) { resolve(null); },
  484. });
  485. });*/
  486. return oXMLHttpRequest('https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id=' + dynId, 'json');
  487. // return oXMLHttpRequest('https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=' + dynId, 'json');
  488. }
  489. async function handleDynamicDownload(dynId) {
  490. // console.log('handleDynamicDownload: ' + dynId);
  491. const dynRes = await getDynamicDetail(dynId);
  492. // console.log(dynRes.data);
  493. if (dynRes.data.card.card) {
  494. const card = JSON.parse(dynRes.data.card.card);
  495. switch(dynRes.data.card.desc.type) {
  496. case 1:
  497. // 转发
  498. break;
  499. case 2:
  500. // 图片
  501. // console.log('picture');
  502. handleImageDynamic(dynRes.data);
  503. break;
  504. case 4:
  505. // 文字
  506. break;
  507. case 8:
  508. // 视频
  509. // console.log('video');
  510. handleVideoDynamic(dynRes.data);
  511. break;
  512. case 64:
  513. // 专栏
  514. handleArticleDynamic(dynRes.data);
  515. break;
  516. case 256:
  517. // 音频
  518. break;
  519. default:
  520. break;
  521. }
  522. } else {
  523. console.log('no content found!', dynRes);
  524. alert('无法下载!');
  525. }
  526. }
  527. function addOpusDownloadButton(card) {
  528. if(card.getElementsByClassName('download-button').length == 0) {
  529. // console.log(card);
  530. const buttonBar = card.getElementsByClassName('bili-tabs__nav__items')[0];
  531. let downloadButton = document.createElement('div');
  532. downloadButton.textContent = '下载';
  533. downloadButton.classList.add('bili-tabs__nav__item');
  534. downloadButton.addEventListener('click', function(event) {
  535. const dynId = window.location.pathname.split('/')[window.location.pathname.split('/').length - 1];
  536. // console.log(dynId);
  537. handleDynamicDownload(dynId);
  538. /*const content = document.body.querySelector('div.opus-module-content');
  539. const list = content.querySelectorAll('div.bili-album__preview__picture__img');
  540. // console.log(list);
  541. for (const item of list) {
  542. let imgUrl = item.style.backgroundImage.split(/"|@/)[1] || item.querySelector('img').src.split('@')[0];
  543. if (imgUrl.startsWith('//')) {
  544. imgUrl = 'https:' + imgUrl;
  545. }
  546. const imgName = imgUrl.split('/')[imgUrl.split('/').length - 1];
  547. // console.log(imgUrl);
  548. // console.log(imgName);
  549. GM_download(imgUrl, imgName);
  550. }
  551. const topAlbum = document.body.querySelector('div.opus-module-top__album');
  552. if (topAlbum) {
  553. const topAlbumIndicatorList = topAlbum.querySelectorAll('div.horizontal-scroll-album__indicator > div > img');
  554. const topAlbumList = topAlbum.querySelectorAll('div.horizontal-scroll-album__pic__img > img');
  555. let topList = topAlbumList;
  556. if (topAlbumIndicatorList.length > 0) topList = topAlbumIndicatorList;
  557. for (const item of topList) {
  558. let imgUrl = item.src.split(/@/)[0];
  559. if (imgUrl.startsWith('//')) {
  560. imgUrl = 'https:' + imgUrl;
  561. }
  562. const imgName = imgUrl.split('/')[imgUrl.split('/').length - 1];
  563. // console.log(imgUrl);
  564. // console.log(imgName);
  565. GM_download(imgUrl, imgName);
  566. }
  567. }*/
  568. });
  569. buttonBar.appendChild(downloadButton);
  570. }
  571. }
  572. function addDownloadButton(card) {
  573. // console.log('addDownloadButton');
  574. if(card.getElementsByClassName('download-button').length == 0) {
  575. if(card.getElementsByClassName('bili-dyn-item__footer').length > 0) {
  576. card.querySelectorAll('div.bili-dyn-item__footer > div.bili-dyn-item__action').forEach((ele) => { ele.style.marginRight = '48px'; });
  577. let buttonBar = card.getElementsByClassName('bili-dyn-item__footer')[0];
  578. let downloadButton = document.createElement('div');
  579. downloadButton.classList.add('bili-dyn-item__action');
  580. downloadButton.classList.add('download-button');
  581. let span = document.createElement('div');
  582. span.classList.add('bili-dyn-action');
  583. let icon = document.createElement('i');
  584. icon.style.width = '20px';
  585. icon.style.height = '20px';
  586. icon.style.transform = 'scale(0.8)';
  587. icon.style.backgroundImage = downloadIcon;
  588. icon.style.backgroundRepeat = 'no-repeat';
  589. icon.style.backgroundSize = '100% 100%';
  590. icon.style.backgroundPosition = 'center';
  591. let text = document.createElement('span');
  592. text.textContent = '下载';
  593. span.appendChild(icon);
  594. span.appendChild(text);
  595. downloadButton.appendChild(span);
  596. buttonBar.appendChild(downloadButton);
  597. downloadButton = buttonBar.getElementsByClassName('download-button')[0];
  598. downloadButton.addEventListener('mouseover', function(event) {
  599. this.querySelector('i').style.backgroundImage = downloadIcon;
  600. // console.log('over');
  601. });
  602. downloadButton.addEventListener('mouseout', function(event) {
  603. this.querySelector('i').style.backgroundImage = downloadIcon;
  604. // console.log('out');
  605. });
  606. downloadButton.addEventListener('click', async function(event) {
  607. // console.log('click');
  608. event.preventDefault();
  609. const content = this.closest('div.bili-dyn-item__main');
  610. // console.log(content);
  611. const opusCard = content.querySelector('[dyn-id]');
  612. // console.log(opusCard.getAttribute('dyn-id'));
  613. const dynId = opusCard.getAttribute('dyn-id');
  614. handleDynamicDownload(dynId);
  615. /*const list = content.querySelectorAll('div.bili-album__preview__picture,div.preview__picture__img.b-img');
  616. // console.log(list);
  617. if (list.length > 0) {
  618. for (let j = 0; j < list.length; j++) {
  619. let imgUrl;
  620. if (list[j].querySelector('img')) {
  621. imgUrl = list[j].querySelector('img').src.split(/@/)[0];
  622. } else {
  623. imgUrl = list[j].style.backgroundImage.split(/"|@/)[1];
  624. }
  625. // console.log(imgUrl);
  626. if (imgUrl.startsWith('//')) {
  627. imgUrl = 'https:' + imgUrl;
  628. }
  629. const imgName = imgUrl.split('/')[imgUrl.split('/').length - 1];
  630. // console.log(imgName);
  631. GM_download(imgUrl, imgName);
  632. }
  633. }*/
  634. });
  635. } else if(GM_getValue('enableVideoDownload', false) && card.getElementsByClassName('bili-tabs__nav__items').length > 0) {
  636. // console.log('add video dynamic download button');
  637. const buttonBar = card.getElementsByClassName('bili-tabs__nav__items')[0];
  638. let downloadButton = document.createElement('div');
  639. downloadButton.textContent = '下载';
  640. downloadButton.classList.add('bili-tabs__nav__item');
  641. downloadButton.addEventListener('click', function(event) {
  642. // console.log('click');
  643. event.preventDefault();
  644. const content = this.closest('div.card');
  645. // console.log(content);
  646. const opusCard = content.querySelector('[dyn-id]');
  647. // console.log(opusCard.getAttribute('dyn-id'));
  648. const dynId = opusCard.getAttribute('dyn-id');
  649. handleDynamicDownload(dynId);
  650. /*const list = content.querySelectorAll('div.bili-album__preview__picture,div.preview__picture__img.b-img');
  651. // console.log(list);
  652. if (list.length > 0) {
  653. for (let j = 0; j < list.length; j++) {
  654. let imgUrl;
  655. if (list[j].querySelector('img')) {
  656. imgUrl = list[j].querySelector('img').src.split(/@/)[0];
  657. } else {
  658. imgUrl = list[j].style.backgroundImage.split(/"|@/)[1];
  659. }
  660. // console.log(imgUrl);
  661. if (imgUrl.startsWith('//')) {
  662. imgUrl = 'https:' + imgUrl;
  663. }
  664. const imgName = imgUrl.split('/')[imgUrl.split('/').length - 1];
  665. // console.log(imgName);
  666. GM_download(imgUrl, imgName);
  667. }
  668. }*/
  669. });
  670. buttonBar.appendChild(downloadButton);
  671. }
  672. }
  673. }
  674. function addPlayPageDownloadButton(buttonBar) {
  675. let buttonWrap = document.createElement('div');
  676. buttonWrap.className = 'toolbar-left-item-wrap';
  677. let button = document.createElement('div');
  678. button.className = 'video-toolbar-left-item download-button';
  679. let icon = document.createElement('i');
  680. icon.className = 'video-toolbar-item-icon';
  681. icon.style.width = '36px';
  682. icon.style.height = '36px';
  683. icon.style.backgroundImage = downloadIcon;
  684. icon.style.backgroundRepeat = 'no-repeat';
  685. icon.style.backgroundSize = '100% 100%';
  686. icon.style.backgroundPosition = 'center';
  687. let text = document.createElement('span');
  688. text.textContent = '下载';
  689. button.appendChild(icon);
  690. button.appendChild(text);
  691. buttonWrap.appendChild(button);
  692. buttonBar.appendChild(buttonWrap);
  693. button.addEventListener('click', async function(event) {
  694. // console.log('click');
  695. event.preventDefault();
  696. const bvid = window.location.pathname.match(/BV[a-z|A-Z|0-9]{10}/g)[0];
  697. // console.log(bvid);
  698. handleVideoDownload(bvid);
  699. });
  700. }
  701. function handleOpusCard(card) {
  702. // console.log('handleOpusCard');
  703. if (card.getElementsByClassName('bili-album').length > 0 || card.getElementsByClassName('horizontal-scroll-album').length > 0 || (GM_getValue('enableVideoDownload', false) && card.getElementsByClassName('bili-dyn-card-video').length > 0)) {
  704. addOpusDownloadButton(card);
  705. }
  706. }
  707. function handleCard(card) {
  708. // console.log('handleCard');
  709. if (card.getElementsByClassName('bili-album').length > 0 || card.getElementsByClassName('bili-dyn-gallery').length > 0) {
  710. // console.log('add download button');
  711. card.getElementsByClassName('bili-album__preview__picture__img').forEach((img) => {
  712. img.addEventListener('click', function(event) {
  713. addDownloadButton(card);
  714. });
  715. });
  716. addDownloadButton(card);
  717. } else if (GM_getValue('enableVideoDownload', false) && card.getElementsByClassName('bili-dyn-card-video').length > 0 && card.getElementsByClassName('bili-dyn-action like disabled').length === 0) {
  718. addDownloadButton(card);
  719. }
  720. }
  721. function bodyMouseOver(event) {
  722. // console.log('bodyMouseOver');
  723. if (notLoaded) {
  724. // console.log('not loaded');
  725. if (document.body.querySelector('div.bili-dyn-list')) {
  726. // console.log('feed');
  727. const cards = document.body.querySelectorAll('div.bili-dyn-list div.bili-dyn-item');
  728. // console.log(cards.length);
  729. if (cards.length > cardsTotal) {
  730. // console.log('cards');
  731. cardsTotal = cards.length;
  732. // console.log(cardsTotal);
  733. for (let i = 0; i < cardsTotal; i++) {
  734. // console.log('card');
  735. handleCard(cards[i])
  736. // startIndex += 1;
  737. }
  738. if (cardsTotal > 0) {
  739. notLoaded = false;
  740. }
  741. // document.body.removeEventListener('mouseover', bodyMouseOver);
  742. }
  743. } else if (location.pathname.startsWith('/v/topic/detail') && document.body.querySelector('div.list-view.topic-list__flow-list')) {
  744. const cards = document.body.querySelectorAll('div.list-view.topic-list__flow-list div.bili-dyn-item');
  745. console.log(cards.length);
  746. if (cards.length > cardsTotal) {
  747. // console.log('cards');
  748. cardsTotal = cards.length;
  749. // console.log(cardsTotal);
  750. for (let i = 0; i < cardsTotal; i++) {
  751. // console.log('card');
  752. handleCard(cards[i])
  753. // startIndex += 1;
  754. }
  755. if (cardsTotal > 0) {
  756. notLoaded = false;
  757. }
  758. document.body.removeEventListener('mouseover', bodyMouseOver);
  759. }
  760. } else if(document.body.querySelector('div.bili-dyn-item')) {
  761. // console.log('found single card');
  762. const card = document.body.querySelector('div.bili-dyn-item');
  763. if (card) {
  764. handleCard(card);
  765. notLoaded = false;
  766. // document.body.removeEventListener('mouseover', bodyMouseOver);
  767. }
  768. } else if (document.body.querySelector('div.opus-detail')) {
  769. // console.log('found single opus card');
  770. const card = document.body.querySelector('div.opus-detail');
  771. if (card) {
  772. handleOpusCard(card);
  773. notLoaded = false;
  774. // document.body.removeEventListener('mouseover', bodyMouseOver);
  775. }
  776. } /* else if (GM_getValue('enableVideoDownload', false) && document.body.querySelector('div.video-toolbar-left-main')) {
  777. const buttonBar = document.body.querySelector('div.video-toolbar-left-main');
  778. if (buttonBar) {
  779. addPlayPageDownloadButton(buttonBar);
  780. notLoaded = false;
  781. }
  782. }*/
  783. }
  784. }
  785. if (location.pathname.startsWith('/v/topic/detail')) document.body.addEventListener('mouseover', bodyMouseOver);
  786. function showModal(event) {
  787. // console.log(addDlBtnMode);
  788. let bg = document.createElement('div');
  789. bg.style.position = 'fixed';
  790. bg.style.top = 0;
  791. bg.style.left = 0;
  792. bg.style.zIndex = 500;
  793. bg.style.backgroundColor = 'black';
  794. bg.style.opacity = 0.5;
  795. let modal = document.createElement('div');
  796. document.body.appendChild(bg);
  797. modal.style.position = 'fixed';
  798. modal.style.width = '25rem';
  799. modal.style.height = 'auto';
  800. modal.style.maxHeight = '80vh';
  801. modal.style.zIndex = 600;
  802. modal.style.backgroundColor = 'white';
  803. modal.style.borderStyle = 'solid';
  804. modal.style.borderWidth = '0.2rem';
  805. modal.style.borderRadius = '0.5rem';
  806. modal.style.borderColor = 'black';
  807. modal.style.overflowX = 'hidden';
  808. modal.style.overflowY = 'auto';
  809. modal.style.fontSize = '1rem';
  810. let titleBar = document.createElement('div');
  811. titleBar.textContent = '欢迎使用“下载Bilibili动态页面图片”脚本';
  812. titleBar.style.width = '100%';
  813. titleBar.style.textAlign = 'center';
  814. titleBar.style.backgroundColor = 'black';
  815. titleBar.style.color = 'white';
  816. titleBar.style.fontSize = '1rem';
  817. titleBar.style.fontWeight = 'bold';
  818. titleBar.style.paddingTop = '0.5rem';
  819. titleBar.style.paddingBottom = '0.5rem';
  820. titleBar.style.borderTopLeftRadius = '0.3rem';
  821. titleBar.style.borderTopRightRadius = '0.3rem';
  822. modal.appendChild(titleBar);
  823. let question1 = document.createElement('p');
  824. question1.style.paddingLeft = '2rem';
  825. question1.style.paddingRight = '2rem';
  826. question1.style.marginTop = '1rem';
  827. question1.style.marginBottom = '1rem';
  828. let labelPicName = document.createElement('label');
  829. labelPicName.textContent = '下载图片文件名';
  830. labelPicName.setAttribute('for', 'dlPicName');
  831. question1.appendChild(labelPicName);
  832. let inputPicName = document.createElement('input');
  833. inputPicName.type = 'text';
  834. inputPicName.id = 'dlPicName';
  835. inputPicName.name = 'dlPicName';
  836. inputPicName.style.marginTop = '0.5rem';
  837. inputPicName.style.width = 'calc(100% - 1rem)';
  838. inputPicName.style.padding = '0.1rem 0.2rem 0.1rem 0.2rem';
  839. inputPicName.style.borderStyle = 'solid';
  840. inputPicName.style.borderColor = 'gray';
  841. inputPicName.style.borderWidth = '0.14rem';
  842. inputPicName.style.borderRadius = '0.2rem';
  843. inputPicName.defaultValue = GM_getValue('dlPicName', '{original}.{ext}');
  844. question1.appendChild(inputPicName);
  845. let PicNameExplain1 = document.createElement('p');
  846. PicNameExplain1.innerHTML = '{original} - 原文件名\n{username} - UP主名称\n{userid} - UP主ID\n{dynamicid} - 动态id\n{ext} - 文件后缀\n{index} - 图片序号\n{YYYY} {MM} {DD} {HH} {mm} {ss} - 原博发布时\n间的年份、月份、日期、小时、分钟、秒,可\n分开独立使用\n{content} - 动态文字内容(最多前25个字符)';
  847. PicNameExplain1.style.marginTop = '0.5rem';
  848. PicNameExplain1.style.marginBottom = '0';
  849. PicNameExplain1.style.whiteSpace = 'pre';
  850. PicNameExplain1.style.color = 'gray';
  851. question1.appendChild(PicNameExplain1);
  852. /*let PicNameExplain2 = document.createElement('p');
  853. PicNameExplain2.innerHTML = '<b>注意</b>:启用“打包下载”时,需区分多文件名称,\n避免重复而导致打包后只有一个文件,文件命\n名时,必须包含{original}、{index}中至少一个\n标签。';
  854. PicNameExplain2.style.marginTop = '0.5rem';
  855. PicNameExplain2.style.whiteSpace = 'pre';
  856. PicNameExplain2.style.color = 'gray';
  857. question1.appendChild(PicNameExplain2);*/
  858. modal.appendChild(question1);
  859. let question2 = document.createElement('p');
  860. question2.style.paddingLeft = '2rem';
  861. question2.style.paddingRight = '2rem';
  862. question2.style.marginTop = '1rem';
  863. question2.style.marginBottom = '0';
  864. let labelVideoDownload = document.createElement('label');
  865. labelVideoDownload.setAttribute('for', 'enableVideoDownload');
  866. labelVideoDownload.textContent = '开启视频下载';
  867. labelVideoDownload.style.display = 'inline-block';
  868. labelVideoDownload.style.paddingRight = '0.2rem';
  869. question2.appendChild(labelVideoDownload);
  870. let inputVideoDownload = document.createElement('input');
  871. inputVideoDownload.type = 'checkbox';
  872. inputVideoDownload.id = 'enableVideoDownload';
  873. inputVideoDownload.checked = GM_getValue('enableVideoDownload', false);
  874. question2.appendChild(inputVideoDownload);
  875. let videoDownloadExplain = document.createElement('p');
  876. videoDownloadExplain.textContent = '目前Bilibili视频单文件下载最高只支持720P MP4格式。';
  877. videoDownloadExplain.style.marginTop = '0.5rem';
  878. videoDownloadExplain.style.marginBottom = '0';
  879. videoDownloadExplain.style.color = 'gray';
  880. question2.appendChild(videoDownloadExplain);
  881. let labelVidName = document.createElement('label');
  882. labelVidName.textContent = '下载图片文件名';
  883. labelVidName.setAttribute('for', 'dlVidName');
  884. labelVidName.style.display = 'block';
  885. labelVidName.style.marginTop = '0.5rem';
  886. labelVidName.style.color = GM_getValue('enableVideoDownload', false) ? null : 'gray';
  887. question2.appendChild(labelVidName);
  888. let inputVidName = document.createElement('input');
  889. inputVidName.type = 'text';
  890. inputVidName.id = 'dlVidName';
  891. inputVidName.name = 'dlVidName';
  892. inputVidName.style.marginTop = '0.5rem';
  893. inputVidName.style.width = 'calc(100% - 1rem)';
  894. inputVidName.style.padding = '0.1rem 0.2rem 0.1rem 0.2rem';
  895. inputVidName.style.borderStyle = 'solid';
  896. inputVidName.style.borderWidth = '0.14rem';
  897. inputVidName.style.borderRadius = '0.2rem';
  898. inputVidName.disabled = GM_getValue('enableVideoDownload', false) ? false : true;
  899. inputVidName.style.borderColor = GM_getValue('enableVideoDownload', false) ? 'gray' : 'lightgray';
  900. inputVidName.defaultValue = GM_getValue('dlVidName', '{original}.{ext}');
  901. question2.appendChild(inputVidName);
  902. let vidNameExplain1 = document.createElement('p');
  903. vidNameExplain1.innerHTML = '{original} - 原文件名\n{username} - UP主名称\n{userid} - UP主ID\n{bvid} - 视频BVID\n{aid} - 视频AID\n{cid} - 视频CID\n{title} - 视频标题\n{ext} - 文件后缀\n{YYYY} {MM} {DD} {HH} {mm} {ss} - 原博发布时\n间的年份、月份、日期、小时、分钟、秒,可\n分开独立使用\n{content} - 视频简介(最多前25个字符)';
  904. vidNameExplain1.style.marginTop = '0.5rem';
  905. vidNameExplain1.style.marginBottom = '0';
  906. vidNameExplain1.style.whiteSpace = 'pre';
  907. vidNameExplain1.style.color = 'gray';
  908. question2.appendChild(vidNameExplain1);
  909. modal.appendChild(question2);
  910. /*let question3 = document.createElement('p');
  911. question3.style.paddingLeft = '2rem';
  912. question3.style.paddingRight = '2rem';
  913. question3.style.marginTop = '1rem';
  914. question3.style.marginBottom = '0';
  915. let labelZipMode = document.createElement('label');
  916. labelZipMode.setAttribute('for', 'zipMode');
  917. labelZipMode.textContent = '打包下载';
  918. labelZipMode.style.display = 'inline-block';
  919. labelZipMode.style.paddingRight = '0.2rem';
  920. labelZipMode.style.color = GM_getValue('ariaMode', false) ? 'gray' : null;
  921. question3.appendChild(labelZipMode);
  922. let inputZipMode = document.createElement('input');
  923. inputZipMode.type = 'checkbox';
  924. inputZipMode.id = 'zipMode';
  925. inputZipMode.checked = GM_getValue('zipMode', false);
  926. inputZipMode.disabled = GM_getValue('ariaMode', false);
  927. question3.appendChild(inputZipMode);
  928. let labelPackName = document.createElement('label');
  929. labelPackName.textContent = '打包文件名';
  930. labelPackName.setAttribute('for', 'packFileName');
  931. labelPackName.style.display = 'block';
  932. labelPackName.style.marginTop = '0.5rem';
  933. labelPackName.style.color = (GM_getValue('zipMode', false) && !GM_getValue('ariaMode', false)) ? null : 'gray';
  934. // labelPackName.style.display = GM_getValue('zipMode', false) ? 'block' : 'none';
  935. question3.appendChild(labelPackName);
  936. let inputPackName = document.createElement('input');
  937. inputPackName.type = 'text';
  938. inputPackName.id = 'packFileName';
  939. inputPackName.name = 'packFileName';
  940. inputPackName.style.marginTop = '0.5rem';
  941. inputPackName.style.width = 'calc(100% - 1rem)';
  942. inputPackName.style.padding = '0.1rem 0.2rem 0.1rem 0.2rem';
  943. inputPackName.style.borderStyle = 'solid';
  944. inputPackName.style.borderColor = (GM_getValue('zipMode', false) && !GM_getValue('ariaMode', false)) ? 'gray' : 'lightgray';
  945. inputPackName.style.borderWidth = '0.14rem';
  946. inputPackName.style.borderRadius = '0.2rem';
  947. inputPackName.defaultValue = GM_getValue('packFileName', '{mblogid}.zip');
  948. // inputPackName.style.display = GM_getValue('zipMode', false) ? 'block' : 'none';
  949. inputPackName.disabled = (GM_getValue('zipMode', false) && !GM_getValue('ariaMode', false)) ? false : true;
  950. question3.appendChild(inputPackName);
  951. let filePackExplain = document.createElement('p');
  952. filePackExplain.textContent = '与“下载文件名称”规则相同,但{original}、{ext}、{index}除外';
  953. filePackExplain.style.marginTop = '0.5rem';
  954. filePackExplain.style.marginBottom = '0';
  955. filePackExplain.style.color = 'gray';
  956. // filePackExplain.style.display = GM_getValue('zipMode', false) ? 'block' : 'none';
  957. question3.appendChild(filePackExplain);
  958. modal.appendChild(question3);
  959. let question4 = document.createElement('p');
  960. question4.style.paddingLeft = '2rem';
  961. question4.style.paddingRight = '2rem';
  962. question4.style.marginTop = '1rem';
  963. question4.style.marginBottom = '0';
  964. let labelRetweetMode = document.createElement('label');
  965. labelRetweetMode.setAttribute('for', 'retweetMode');
  966. labelRetweetMode.textContent = '单独设置转发微博下载文件名称';
  967. labelRetweetMode.style.display = 'inline-block';
  968. labelRetweetMode.style.paddingRight = '0.2rem';
  969. question4.appendChild(labelRetweetMode);
  970. let inputRetweetMode = document.createElement('input');
  971. inputRetweetMode.type = 'checkbox';
  972. inputRetweetMode.id = 'retweetMode';
  973. inputRetweetMode.checked = GM_getValue('retweetMode', false);
  974. question4.appendChild(inputRetweetMode);
  975. let labelRetweetFileName = document.createElement('label');
  976. labelRetweetFileName.textContent = '转发微博下载文件名称';
  977. labelRetweetFileName.setAttribute('for', 'retweetFileName');
  978. labelRetweetFileName.style.display = 'block';
  979. labelRetweetFileName.style.marginTop = '0.5rem';
  980. labelRetweetFileName.style.color = GM_getValue('retweetMode', false) ? null : 'gray';
  981. // labelPackName.style.display = GM_getValue('retweetMode', false) ? 'block' : 'none';
  982. question4.appendChild(labelRetweetFileName);
  983. let inputRetweetFileName = document.createElement('input');
  984. inputRetweetFileName.type = 'text';
  985. inputRetweetFileName.id = 'retweetFileName';
  986. inputRetweetFileName.name = 'retweetFileName';
  987. inputRetweetFileName.style.marginTop = '0.5rem';
  988. inputRetweetFileName.style.width = 'calc(100% - 1rem)';
  989. inputRetweetFileName.style.padding = '0.1rem 0.2rem 0.1rem 0.2rem';
  990. inputRetweetFileName.style.borderStyle = 'solid';
  991. inputRetweetFileName.style.borderColor = 'lightgray';
  992. inputRetweetFileName.style.borderWidth = '0.14rem';
  993. inputRetweetFileName.style.borderRadius = '0.2rem';
  994. inputRetweetFileName.defaultValue = GM_getValue('retweetFileName', '{original}.{ext}');
  995. // inputRetweetFileName.style.display = GM_getValue('retweetMode', false) ? 'block' : 'none';
  996. inputRetweetFileName.disabled = GM_getValue('retweetMode', false) ? false : true;
  997. question4.appendChild(inputRetweetFileName);
  998. let retweetFileNameExplain = document.createElement('p');
  999. retweetFileNameExplain.textContent = '除“下载文件名”规则外,额外标签如下:\n{re.mblogid} - 转博mblogid\n{re.username} - 转发博主名称\n{re.userid} - 转发博主ID\n{re.uid} - 转博uid\n{re.content} - 转发博文内容(最多前25个字符)\n{re.YYYY} {re.MM} {re.DD} {re.HH} {re.mm} {re.ss}\n - 原博发布时间的年份、月份、日期、小时、\n分钟、秒,可分开独立使用';
  1000. retweetFileNameExplain.style.marginTop = '0.5rem';
  1001. retweetFileNameExplain.style.whiteSpace = 'pre';
  1002. retweetFileNameExplain.style.marginBottom = '0';
  1003. retweetFileNameExplain.style.color = 'gray';
  1004. // retweetFileNameExplain.style.display = GM_getValue('retweetMode', false) ? 'block' : 'none';
  1005. question4.appendChild(retweetFileNameExplain);
  1006. let labelRetweetPackName = document.createElement('label');
  1007. labelRetweetPackName.textContent = '转发微博打包文件名';
  1008. labelRetweetPackName.setAttribute('for', 'retweetPackFileName');
  1009. labelRetweetPackName.style.display = 'block';
  1010. labelRetweetPackName.style.marginTop = '0.5rem';
  1011. labelRetweetPackName.style.color = (GM_getValue('zipMode', false) && GM_getValue('retweetMode', false) && !GM_getValue('ariaMode', false)) ? null : 'gray';
  1012. // labelRetweetPackName.style.display = GM_getValue('zipMode', false) ? 'block' : 'none';
  1013. question4.appendChild(labelRetweetPackName);
  1014. let inputRetweetPackName = document.createElement('input');
  1015. inputRetweetPackName.type = 'text';
  1016. inputRetweetPackName.id = 'retweetPackFileName';
  1017. inputRetweetPackName.name = 'retweetPackFileName';
  1018. inputRetweetPackName.style.marginTop = '0.5rem';
  1019. inputRetweetPackName.style.width = 'calc(100% - 1rem)';
  1020. inputRetweetPackName.style.padding = '0.1rem 0.2rem 0.1rem 0.2rem';
  1021. inputRetweetPackName.style.borderStyle = 'solid';
  1022. inputRetweetPackName.style.borderColor = 'lightgray';
  1023. inputRetweetPackName.style.borderWidth = '0.14rem';
  1024. inputRetweetPackName.style.borderRadius = '0.2rem';
  1025. inputRetweetPackName.defaultValue = GM_getValue('retweetPackFileName', '{mblogid}.zip');
  1026. // inputRetweetPackName.style.display = (GM_getValue('zipMode', false) && GM_getValue('retweetMode', false) && !GM_getValue('ariaMode', false)) ? 'block' : 'none';
  1027. inputRetweetPackName.disabled = (GM_getValue('zipMode', false) && GM_getValue('retweetMode', false) && !GM_getValue('ariaMode', false)) ? false : true;
  1028. question4.appendChild(inputRetweetPackName);
  1029. let retweetPackExplain = document.createElement('p');
  1030. retweetPackExplain.textContent = '与“转发微博下载文件名称”规则相同,但{original}、{ext}、{index}除外';
  1031. retweetPackExplain.style.marginTop = '0.5rem';
  1032. retweetPackExplain.style.marginBottom = '0';
  1033. retweetPackExplain.style.color = 'gray';
  1034. // retweetPackExplain.style.display = (GM_getValue('zipMode', false) && GM_getValue('retweetMode', false) && !GM_getValue('ariaMode', false)) ? 'block' : 'none';
  1035. question4.appendChild(retweetPackExplain);
  1036. modal.appendChild(question4);
  1037. let question5 = document.createElement('p');
  1038. question5.style.paddingLeft = '2rem';
  1039. question5.style.paddingRight = '2rem';
  1040. question5.style.marginTop = '1rem';
  1041. question5.style.marginBottom = '0';
  1042. let labelAriaMode = document.createElement('label');
  1043. labelAriaMode.setAttribute('for', 'ariaMode');
  1044. labelAriaMode.textContent = '使用Aria2c远程下载';
  1045. labelAriaMode.style.display = 'inline-block';
  1046. labelAriaMode.style.paddingRight = '0.2rem';
  1047. question5.appendChild(labelAriaMode);
  1048. let inputAriaMode = document.createElement('input');
  1049. inputAriaMode.type = 'checkbox';
  1050. inputAriaMode.id = 'ariaMode';
  1051. inputAriaMode.checked = GM_getValue('ariaMode', false);
  1052. question5.appendChild(inputAriaMode);
  1053. let ariaModeExplain = document.createElement('p');
  1054. ariaModeExplain.textContent = '使用此方式下载,无法使用打包功能,无法在页面右下角显示下载进度和结果。';
  1055. ariaModeExplain.style.marginTop = '0.5rem';
  1056. ariaModeExplain.style.marginBottom = '0';
  1057. ariaModeExplain.style.color = 'gray';
  1058. question5.appendChild(ariaModeExplain);
  1059. let labelAriaRpcUrl = document.createElement('label');
  1060. labelAriaRpcUrl.textContent = 'RPC接口地址';
  1061. labelAriaRpcUrl.setAttribute('for', 'ariaRpcUrl');
  1062. labelAriaRpcUrl.style.display = 'block';
  1063. labelAriaRpcUrl.style.marginTop = '0.5rem';
  1064. labelAriaRpcUrl.style.color = GM_getValue('ariaMode', false) ? null : 'gray';
  1065. question5.appendChild(labelAriaRpcUrl);
  1066. let inputAriaRpcUrl = document.createElement('input');
  1067. inputAriaRpcUrl.type = 'text';
  1068. inputAriaRpcUrl.id = 'ariaRpcUrl';
  1069. inputAriaRpcUrl.name = 'ariaRpcUrl';
  1070. inputAriaRpcUrl.style.marginTop = '0.5rem';
  1071. inputAriaRpcUrl.style.width = 'calc(100% - 1rem)';
  1072. inputAriaRpcUrl.style.padding = '0.1rem 0.2rem 0.1rem 0.2rem';
  1073. inputAriaRpcUrl.style.borderStyle = 'solid';
  1074. inputAriaRpcUrl.style.borderColor = 'lightgray';
  1075. inputAriaRpcUrl.style.borderWidth = '0.14rem';
  1076. inputAriaRpcUrl.style.borderRadius = '0.2rem';
  1077. inputAriaRpcUrl.defaultValue = GM_getValue('ariaRpcUrl', 'http://localhost:6800/jsonrpc');
  1078. // inputAriaRpcUrl.style.display = GM_getValue('ariaMode', false) ? 'block' : 'none';
  1079. inputAriaRpcUrl.disabled = GM_getValue('ariaMode', false) ? false : true;
  1080. question5.appendChild(inputAriaRpcUrl);
  1081. let inputAriaExplain = document.createElement('p');
  1082. inputAriaExplain.textContent = '如果接口地址不是localhost,需手动将地址添加到XHR白名单。';
  1083. inputAriaExplain.style.marginTop = '0.5rem';
  1084. inputAriaExplain.style.marginBottom = '0';
  1085. inputAriaExplain.style.color = 'gray';
  1086. question5.appendChild(inputAriaExplain);
  1087. modal.appendChild(question5);
  1088. inputRetweetMode.addEventListener('change', function(event) {
  1089. if (event.currentTarget.checked) {
  1090. // labelRetweetFileName.style.display = 'block';
  1091. // inputRetweetFileName.style.display = 'block';
  1092. // retweetFileNameExplain.style.display = 'block';
  1093. inputRetweetFileName.disabled = false;
  1094. labelRetweetFileName.style.color = null;
  1095. inputRetweetFileName.style.borderColor = 'gray';
  1096. } else {
  1097. // labelRetweetFileName.style.display = 'none';
  1098. // inputRetweetFileName.style.display = 'none';
  1099. // retweetFileNameExplain.style.display = 'none';
  1100. inputRetweetFileName.disabled = true;
  1101. labelRetweetFileName.style.color = 'gray';
  1102. inputRetweetFileName.style.borderColor = 'lightgray';
  1103. }
  1104. if (event.currentTarget.checked && inputZipMode.checked && !inputAriaMode.checked) {
  1105. inputRetweetPackName.disabled = false;
  1106. labelRetweetPackName.style.color = null;
  1107. inputRetweetPackName.style.borderColor = 'gray';
  1108. } else {
  1109. inputRetweetPackName.disabled = true;
  1110. labelRetweetPackName.style.color = 'gray';
  1111. inputRetweetPackName.style.borderColor = 'lightgray';
  1112. }
  1113. });
  1114. inputZipMode.addEventListener('change', function(event) {
  1115. if (event.currentTarget.checked) {
  1116. // labelPackName.style.display = 'block';
  1117. // inputPackName.style.display = 'block';
  1118. // filePackExplain.style.display = 'block';
  1119. inputPackName.disabled = false;
  1120. labelPackName.style.color = null;
  1121. inputPackName.style.borderColor = 'gray';
  1122. } else {
  1123. // labelPackName.style.display = 'none';
  1124. // inputPackName.style.display = 'none';
  1125. // filePackExplain.style.display = 'none';
  1126. inputPackName.disabled = true;
  1127. labelPackName.style.color = 'gray';
  1128. inputPackName.style.borderColor = 'lightgray';
  1129. }
  1130. if (event.currentTarget.checked && inputRetweetMode.checked) {
  1131. inputRetweetPackName.disabled = false;
  1132. labelRetweetPackName.style.color = null;
  1133. inputRetweetPackName.style.borderColor = 'gray';
  1134. } else {
  1135. inputRetweetPackName.disabled = true;
  1136. labelRetweetPackName.style.color = 'gray';
  1137. inputRetweetPackName.style.borderColor = 'lightgray';
  1138. }
  1139. });
  1140. inputAriaMode.addEventListener('change', function(event) {
  1141. if (event.currentTarget.checked) {
  1142. // labelAriaRpcUrl.style.display = 'block';
  1143. // inputAriaRpcUrl.style.display = 'block';
  1144. inputAriaRpcUrl.disabled = false;
  1145. labelAriaRpcUrl.style.color = null;
  1146. inputAriaRpcUrl.style.borderColor = 'gray';
  1147. inputZipMode.disabled = true;
  1148. labelZipMode.style.color = 'gray';
  1149. } else {
  1150. // labelAriaRpcUrl.style.display = 'none';
  1151. // inputAriaRpcUrl.style.display = 'none';
  1152. inputAriaRpcUrl.disabled = true;
  1153. labelAriaRpcUrl.style.color = 'gray';
  1154. inputAriaRpcUrl.style.borderColor = 'lightgray';
  1155. inputZipMode.disabled = false;
  1156. labelZipMode.style.color = null;
  1157. }
  1158. if (!event.currentTarget.checked && inputZipMode.checked) {
  1159. inputPackName.disabled = false;
  1160. labelPackName.style.color = null;
  1161. inputPackName.style.borderColor = 'gray';
  1162. } else {
  1163. inputPackName.disabled = true;
  1164. labelPackName.style.color = 'gray';
  1165. inputPackName.style.borderColor = 'lightgray';
  1166. }
  1167. if (!event.currentTarget.checked && inputZipMode.checked && inputRetweetMode.checked) {
  1168. inputRetweetPackName.disabled = false;
  1169. labelRetweetPackName.style.color = null;
  1170. inputRetweetPackName.style.borderColor = 'gray';
  1171. } else {
  1172. inputRetweetPackName.disabled = true;
  1173. labelRetweetPackName.style.color = 'gray';
  1174. inputRetweetPackName.style.borderColor = 'lightgray';
  1175. }
  1176. });*/
  1177. inputVideoDownload.addEventListener('change', function(event) {
  1178. if (event.currentTarget.checked) {
  1179. // labelRetweetFileName.style.display = 'block';
  1180. // inputRetweetFileName.style.display = 'block';
  1181. // retweetFileNameExplain.style.display = 'block';
  1182. inputVidName.disabled = false;
  1183. labelVidName.style.color = null;
  1184. inputVidName.style.borderColor = 'gray';
  1185. } else {
  1186. // labelRetweetFileName.style.display = 'none';
  1187. // inputRetweetFileName.style.display = 'none';
  1188. // retweetFileNameExplain.style.display = 'none';
  1189. inputVidName.disabled = true;
  1190. labelVidName.style.color = 'gray';
  1191. inputVidName.style.borderColor = 'lightgray';
  1192. }
  1193. });
  1194. let okButton = document.createElement('button');
  1195. okButton.textContent = '确定';
  1196. okButton.style.paddingTop = '0.5rem';
  1197. okButton.style.paddingBottom = '0.5rem';
  1198. okButton.style.margin = '2rem';
  1199. okButton.style.backgroundColor = 'darkblue';
  1200. okButton.style.color = 'white';
  1201. okButton.style.fontSize = '1.5rem';
  1202. okButton.style.fontWeight = 'bold';
  1203. okButton.style.width = '21rem';
  1204. okButton.style.borderStyle = 'solid';
  1205. okButton.style.borderRadius = '0.5rem';
  1206. okButton.style.borderColor = 'black';
  1207. okButton.style.borderWidth = '0.2rem';
  1208. okButton.addEventListener('mouseover', function(event) {
  1209. okButton.style.backgroundColor = 'blue';
  1210. });
  1211. okButton.addEventListener('mouseout', function(event) {
  1212. okButton.style.backgroundColor = 'darkblue';
  1213. });
  1214. okButton.addEventListener('mousedown', function(event) {
  1215. okButton.style.backgroundColor = 'darkblue';
  1216. });
  1217. okButton.addEventListener('mouseover', function(event) {
  1218. okButton.style.backgroundColor = 'blue';
  1219. });
  1220. function resizeWindow(event) {
  1221. // console.log('resize');
  1222. bg.style.width = document.documentElement.clientWidth.toString() + 'px';
  1223. bg.style.height = document.documentElement.clientHeight.toString() + 'px';
  1224. modal.style.top = (( document.documentElement.clientHeight - modal.offsetHeight ) / 2).toString() + 'px';
  1225. modal.style.left = (( document.documentElement.clientWidth - modal.offsetWidth ) / 2).toString() + 'px';
  1226. }
  1227. okButton.addEventListener('click', function(event) {
  1228. /*if (document.getElementById('zipMode').checked && !document.getElementById('dlPicName').value.includes('{original}') && !document.getElementById('dlPicName').value.includes('{index}')) {
  1229. alert('启用“打包下载”时,需区分多文件名称,避免重复而导致打包后只有一个文件,文件命名时,必须包含{original}、{index}中至少一个标签。');
  1230. document.getElementById('dlPicName').focus();
  1231. return;
  1232. }*/
  1233. let refreshFlag = false;
  1234. if (document.getElementById('enableVideoDownload').checked !== GM_getValue('enableVideoDownload', false)) {
  1235. refreshFlag = true;
  1236. }
  1237. GM_setValue('dlPicName', document.getElementById('dlPicName').value);
  1238. GM_setValue('enableVideoDownload', document.getElementById('enableVideoDownload').checked);
  1239. GM_setValue('dlVidName', document.getElementById('dlVidName').value);
  1240. // GM_setValue('retweetMode', document.getElementById('retweetMode').checked);
  1241. // GM_setValue('retweetFileName', document.getElementById('retweetFileName').value);
  1242. // GM_setValue('zipMode', document.getElementById('zipMode').checked);
  1243. // GM_setValue('packFileName', document.getElementById('packFileName').value);
  1244. // GM_setValue('retweetPackFileName', document.getElementById('retweetPackFileName').value);
  1245. // GM_setValue('ariaMode', document.getElementById('ariaMode').checked);
  1246. // GM_setValue('ariaRpcUrl', document.getElementById('ariaRpcUrl').value);
  1247. GM_setValue('isSet', settingVersion);
  1248. if (refreshFlag) {
  1249. alert('已' + (document.getElementById('enableVideoDownload').checked ? '开启' : '关闭') + '视频下载功能,将在页面刷新后生效。');
  1250. location.reload();
  1251. }
  1252. window.removeEventListener('resize', resizeWindow);
  1253. document.body.removeChild(modal);
  1254. document.body.removeChild(bg);
  1255. });
  1256. modal.appendChild(okButton);
  1257. document.body.appendChild(modal);
  1258. /*bg.addEventListener('click', function(event) {
  1259. document.body.removeChild(modal);
  1260. document.body.removeChild(bg);
  1261. window.removeEventListener('resize', resizeWindow);
  1262. });*/
  1263. resizeWindow();
  1264. window.addEventListener('resize', resizeWindow);
  1265. }
  1266.  
  1267. if(GM_getValue('isSet', null) !== settingVersion) {
  1268. showModal();
  1269. }
  1270. bodyMouseOver();
  1271. new MutationObserver((mutationList, observer) => {
  1272. for (const mutation of mutationList) {
  1273. // console.log(mutation);
  1274. if (mutation.type === 'childList') {
  1275. // if (!['svg'].includes(mutation.target.tagName)) console.log(mutation.target);
  1276. /*for (const node of mutation.addedNodes) {
  1277. // console.log(node.nodeType, node);
  1278. if (node.nodeType === 1 && Object.keys(mutation.target.getElementsByClassName('video-like')).length > 0) {
  1279. console.log(mutation.target, node);
  1280. }
  1281. }*/
  1282. if (mutation.target.tagName === 'DIV' && ['bili-dyn-list__items', 'content'].includes(mutation.target.className)) {
  1283. // console.log(mutation.addedNodes);
  1284. for (const node of mutation.addedNodes) {
  1285. // console.log(node);
  1286. handleCard(node);
  1287. }
  1288. } else if (mutation.target.tagName === 'DIV' && mutation.target.parentNode.className === 'list-view topic-list__flow-list') {
  1289. // vconsole.log(mutation.addedNodes);
  1290. for (const node of mutation.addedNodes) {
  1291. // console.log(node);
  1292. handleCard(node);
  1293. }
  1294. } else if (GM_getValue('enableVideoDownload', false) && mutation.target.tagName === 'BODY') {
  1295. for (const node of mutation.addedNodes) {
  1296. if (node.nodeType === 1 && node.tagName === 'DIV' && node.classList.contains('lt-row')) {
  1297. // console.log(mutation);
  1298. const buttonBar = document.body.querySelector('div.video-toolbar-left-main');
  1299. if (buttonBar && !buttonBar.querySelector('div.download-button')) {
  1300. // console.log(mutation);
  1301. addPlayPageDownloadButton(buttonBar);
  1302. }
  1303. }
  1304. }
  1305. }
  1306. }
  1307. }
  1308. }).observe(document.body, { attributes: false, childList: true, subtree: true });
  1309.  
  1310. let settingButton = document.createElement('button');
  1311. settingButton.textContent = '设置';
  1312. settingButton.style.position = 'fixed';
  1313. settingButton.style.top = '4rem';
  1314. settingButton.style.left = '0rem';
  1315. settingButton.style.fontSize = '0.7rem';
  1316. settingButton.style.backgroundColor = 'gray';
  1317. settingButton.style.color = 'white';
  1318. settingButton.style.borderWidth = '0.2rem';
  1319. settingButton.style.borderStyle = 'solid';
  1320. settingButton.style.borderRadius = '0.5rem';
  1321. settingButton.style.borderColor = 'lightgrey';
  1322. settingButton.style.zIndex = 400;
  1323. settingButton.style.paddingLeft = '1rem';
  1324. settingButton.style.paddingRight = '1rem';
  1325. settingButton.style.paddingTop = '0.2rem';
  1326. settingButton.style.paddingBottom = '0.2rem';
  1327. settingButton.addEventListener('mouseover', function(event) {
  1328. settingButton.style.backgroundColor = 'darkgray';
  1329. settingButton.style.color = 'black';
  1330. });
  1331. settingButton.addEventListener('mouseout', function(event) {
  1332. settingButton.style.backgroundColor = 'gray';
  1333. settingButton.style.color = 'white';
  1334. });
  1335. settingButton.addEventListener('mousedown', function(event) {
  1336. settingButton.style.backgroundColor = 'gray';
  1337. settingButton.style.color = 'white';
  1338. });
  1339. settingButton.addEventListener('mouseup', function(event) {
  1340. settingButton.style.backgroundColor = 'darkgray';
  1341. settingButton.style.color = 'black';
  1342. });
  1343. settingButton.addEventListener('click', showModal);
  1344. document.body.appendChild(settingButton);
  1345. GM_registerMenuCommand('设置', showModal, "0");
  1346. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址