├── src
├── background.js
├── icon.png
├── manifest.json
└── content_script.js
├── .gitignore
├── doc
└── use.png
└── README.md
/src/background.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
--------------------------------------------------------------------------------
/doc/use.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vacabun/weibo-dl/HEAD/doc/use.png
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vacabun/weibo-dl/HEAD/src/icon.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weibo-dl
2 |
3 | 用于一键下载微博视频/图片的chrome扩展程序
4 |
5 | [chrome应用商店地址](https://chrome.google.com/webstore/detail/weibo%E4%B8%80%E9%94%AE%E4%B8%8B%E8%BD%BD/bkgglniclcgkendelemfbgganhojjknh)
6 |
7 | 使用方法
8 |
9 | 
10 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "weibo一键下载",
4 | "version": "1.0.8",
5 | "description": "一键下载微博视频/图片",
6 | "homepage_url": "https://github.com/vacabun",
7 | "author": "vacabun",
8 | "icons":
9 | {
10 | "16": "icon.png",
11 | "48": "icon.png",
12 | "128": "icon.png"
13 | },
14 | "background": {
15 | "service_worker": "background.js"
16 | },
17 | "content_scripts": [
18 | {
19 | "js": [
20 | "content_script.js"
21 | ],
22 | "matches": [
23 | "https://weibo.com/*",
24 | "https://www.weibo.com/*"
25 | ],
26 | "run_at": "document_start"
27 | }
28 | ],
29 | "permissions": [
30 | "downloads"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/src/content_script.js:
--------------------------------------------------------------------------------
1 | // 唯一ID
2 | var overlayId = crypto.randomUUID();
3 |
4 | /**
5 | * 显示加载框
6 | */
7 | function showLoading() {
8 | var overlay = document.getElementById(overlayId)
9 | overlay.style.display = 'flex';
10 | }
11 |
12 | /**
13 | * 隐藏加载框
14 | */
15 | function hideLoading() {
16 | var overlay = document.getElementById(overlayId)
17 | if (overlay) {
18 | overlay.style.display = 'none';
19 | }
20 | }
21 |
22 | function downloadImage(imageUrl, name) {
23 | this.showLoading();
24 | var xhr = new XMLHttpRequest();
25 | xhr.onreadystatechange = function () {
26 | if (xhr.readyState === 4) {
27 | var blob = new Blob([xhr.response], {type: 'image/jpeg'});
28 | var url = window.URL.createObjectURL(blob);
29 | var a = document.createElement('a');
30 | a.href = url;
31 | a.download = name;
32 | document.body.appendChild(a);
33 | a.click();
34 | setTimeout(function () {
35 | document.body.removeChild(a);
36 | window.URL.revokeObjectURL(url);
37 | this.hideLoading();
38 | }, 0);
39 | }
40 | };
41 | // 处理请求被中止的回调
42 | xhr.onabort = function () {
43 | this.hideLoading();
44 | };
45 | // 处理请求发生错误的回调
46 | xhr.onerror = function () {
47 | this.hideLoading();
48 | };
49 | xhr.open('GET', imageUrl);
50 | xhr.responseType = 'arraybuffer';
51 | xhr.send();
52 | }
53 |
54 | function downloadWrapper(url, name, type) {
55 | if (type == 'video') {
56 | this.showLoading();
57 | fetch(url).then(function (response) {
58 | if (response.ok) {
59 | return response.blob();
60 | } else {
61 | throw new Error('HTTP status code: ' + response.status);
62 | }
63 | })
64 | .then(function (blob) {
65 | // 创建下载链接
66 | var downloadLink = document.createElement('a');
67 | downloadLink.href = URL.createObjectURL(blob);
68 | downloadLink.download = name; // 下载文件的名称
69 |
70 | this.hideLoading();
71 | // 触发点击事件以下载文件
72 | downloadLink.click();
73 |
74 | // 清理资源
75 | URL.revokeObjectURL(downloadLink.href);
76 | })
77 | .catch(function (error) {
78 | console.error(error);
79 | });
80 | }
81 | if (type == 'pic') {
82 | downloadImage(url, name);
83 | }
84 | }
85 |
86 | function handleDownloadList(downloadList) {
87 | for (const item of downloadList) {
88 | downloadWrapper(item.url, item.name, item.type);
89 | }
90 | }
91 |
92 | function getName(nameSetting, originalName, ext, userName, userId, postId, postUid, index, postTime, content) {
93 | let setName = nameSetting;
94 | setName = setName.replace('{ext}', ext);
95 | setName = setName.replace('{original}', originalName);
96 | setName = setName.replace('{username}', userName);
97 | setName = setName.replace('{userid}', userId);
98 | setName = setName.replace('{mblogid}', postId);
99 | setName = setName.replace('{uid}', postUid);
100 | setName = setName.replace('{index}', index);
101 | setName = setName.replace('{content}', content.substring(0, 50));
102 | let YYYY, MM, DD, HH, mm, ss;
103 | const postAt = new Date(postTime);
104 | if (postTime) {
105 | YYYY = postAt.getFullYear().toString();
106 | MM = (postAt.getMonth() + 1).toString().padStart(2, '0');
107 | DD = postAt.getDate().toString().padStart(2, '0');
108 | HH = postAt.getHours().toString().padStart(2, '0');
109 | mm = postAt.getMinutes().toString().padStart(2, '0');
110 | ss = postAt.getSeconds().toString().padStart(2, '0');
111 | }
112 | setName = setName.replace('{YYYY}', YYYY);
113 | setName = setName.replace('{MM}', MM);
114 | setName = setName.replace('{DD}', DD);
115 | setName = setName.replace('{HH}', HH);
116 | setName = setName.replace('{mm}', mm);
117 | setName = setName.replace('{ss}', ss);
118 | return setName.replace(/[<|>|*|"|\/|\|:|?|\n]/g, '_');
119 | }
120 |
121 | function handleVideo(mediaInfo, padLength, userName, userId, postId, postUid, index, postTime, text) {
122 | const newList = [];
123 | let largeVidUrl = mediaInfo.playback_list ? mediaInfo.playback_list[0].play_info.url : mediaInfo.stream_url;
124 | let vidName = largeVidUrl.split('?')[0];
125 | vidName = vidName.split('/')[vidName.split('/').length - 1].split('?')[0];
126 | let originalName = vidName.split('.')[0];
127 | let ext = vidName.split('.')[1];
128 | const setName = getName(dlFileName, originalName, ext, userName, userId, postId, postUid, index.toString().padStart(padLength, '0'), postTime, text);
129 | newList.push({url: largeVidUrl, name: setName, type: 'video'});
130 | if (mediaInfo.hasOwnProperty('pic_info')) {
131 | let picUrl = mediaInfo.pic_info.pic_big.url;
132 | let largePicUrl = picUrl.replace('/orj480/', '/large/');
133 | let picName = largePicUrl.split('/')[largePicUrl.split('/').length - 1].split('?')[0];
134 | let originalName = picName.split('.')[0];
135 | let ext = picName.split('.')[1];
136 | const setName = getName(dlFileName, originalName, ext, userName, userId, postId, postUid, index.toString().padStart(padLength, '0'), postTime, text);
137 | newList.push({url: largePicUrl, name: setName, type: 'pic'});
138 | }
139 | return newList;
140 | }
141 |
142 | function handlePic(pic, padLength, userName, userId, postId, postUid, index, postTime, text) {
143 | let newList = [];
144 | let largePicUrl = pic.largest.url;
145 | let picName = largePicUrl.split('/')[largePicUrl.split('/').length - 1].split('?')[0];
146 | let originalName = picName.split('.')[0];
147 | let ext = picName.split('.')[1];
148 | const setName = getName(dlFileName, originalName, ext, userName, userId, postId, postUid, index.toString().padStart(padLength, '0'), postTime, text);
149 | newList.push({url: largePicUrl, name: setName, headerFlag: true, type: 'pic'});
150 | if (pic.hasOwnProperty('video')) {
151 | let videoUrl = pic.video;
152 | let videoName = videoUrl.split('%2F')[videoUrl.split('%2F').length - 1].split('?')[0];
153 | videoName = videoName.split('/')[videoName.split('/').length - 1].split('?')[0];
154 | if (!videoName.includes('.')) videoName = videoUrl.split('/')[videoUrl.split('/').length - 1].split('?')[0];
155 | let originalName = videoName.split('.')[0];
156 | let ext = videoName.split('.')[1];
157 | const setName = getName(dlFileName, originalName, ext, userName, userId, postId, postUid, index.toString().padStart(padLength, '0'), postTime, text);
158 | newList.push({url: videoUrl, name: setName, type: 'video'});
159 | }
160 | return newList;
161 | }
162 |
163 | function httpGet(theUrl) {
164 | let xmlHttp = new XMLHttpRequest();
165 | xmlHttp.open("GET", theUrl, false); // false for synchronous request
166 | xmlHttp.send(null);
167 | return xmlHttp.responseText;
168 | }
169 |
170 | function addDlBtn(footer) {
171 | let dlBtnDiv = document.createElement('div');
172 | let divInDiv = document.createElement('div');
173 | if (footer.getElementsByClassName('_item_198pe_23').length > 0 ) {
174 | dlBtnDiv.className = 'woo-box-item-flex toolbar_item_1ky_D _item_198pe_23 _cursor_198pe_184';
175 | divInDiv.className = 'woo-box-flex woo-box-alignCenter woo-box-justifyCenter _likebox_198pe_50 _wrap_198pe_137 _likebox_198pe_50 _wrap_198pe_137';
176 | } else {
177 | dlBtnDiv.className = 'woo-box-item-flex toolbar_item_1ky_D toolbar_cursor_34j5V';
178 | divInDiv.className = 'woo-box-flex woo-box-alignCenter woo-box-justifyCenter toolbar_wrap_np6Ug';
179 | }
180 | let dlBtn = document.createElement('button');
181 | dlBtn.className = 'woo-like-main toolbar_btn_Cg9tz download-button';
182 | dlBtn.setAttribute('tabindex', '0');
183 | dlBtn.setAttribute('title', '下载');
184 | dlBtn.innerHTML = '下载';
185 | dlBtn.addEventListener('click', async function (event) {
186 | event.preventDefault();
187 | const article = this.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement;
188 | if (article.tagName.toLowerCase() == 'article') {
189 | const header = article.getElementsByTagName('header')[0];
190 | postLink = header.getElementsByClassName('_time_1tpft_33')[0];
191 | if (header.getElementsByClassName('_time_1tpft_33').length > 0 ) {
192 | postLink = header.getElementsByClassName('_time_1tpft_33')[0];
193 | } else {
194 | postLink = header.getElementsByClassName('head-info_time_6sFQg')[0];
195 | }
196 | let postId = postLink.href.split('/')[postLink.href.split('/').length - 1];
197 | var response;
198 | if (location.host == 'www.weibo.com') {
199 | response = httpGet('https://www.weibo.com/ajax/statuses/show?id=' + postId);
200 | } else {
201 | response = httpGet('https://weibo.com/ajax/statuses/show?id=' + postId);
202 | }
203 | const resJson = JSON.parse(response);
204 | let status = resJson;
205 | if (resJson.hasOwnProperty('retweeted_status')) {
206 | status = resJson.retweeted_status;
207 | }
208 | postId = status.mblogid;
209 | const picInfos = status.pic_infos;
210 | const mixMediaInfo = status.mix_media_info;
211 | const userName = status.user.screen_name;
212 | const userId = status.user.idstr;
213 | const postUid = status.idstr;
214 | const postTime = status.created_at;
215 | const text = status.text_raw;
216 | let downloadList = [];
217 | if (footer.parentElement.getElementsByTagName('video').length > 0) {
218 | if (resJson.hasOwnProperty('page_info')) {
219 | downloadList = downloadList.concat(handleVideo(resJson.page_info.media_info, 1, userName, userId, postId, postUid, 1, postTime, text));
220 | }
221 | }
222 | if (picInfos) {
223 | let index = 0;
224 | let padLength = Object.entries(picInfos).length.toString().length;
225 | for (const [id, pic] of Object.entries(picInfos)) {
226 | index += 1;
227 | downloadList = downloadList.concat(handlePic(pic, padLength, userName, userId, postId, postUid, index, postTime, text));
228 | }
229 | }
230 | if (mixMediaInfo && mixMediaInfo.items) {
231 | let index = 0;
232 | let padLength = Object.entries(mixMediaInfo.items).length.toString().length;
233 | for (const [id, media] of Object.entries(mixMediaInfo.items)) {
234 | index += 1;
235 | if (media.type === 'video') {
236 | downloadList = downloadList.concat(handleVideo(media.data.media_info, 1, userName, userId, postId, postUid, index, postTime, text));
237 | } else if (media.type === 'pic') {
238 | downloadList = downloadList.concat(handlePic(media.data, padLength, userName, userId, postId, postUid, index, postTime, text));
239 | }
240 | }
241 | }
242 | handleDownloadList(downloadList);
243 | }
244 | });
245 | divInDiv.appendChild(dlBtn);
246 | dlBtnDiv.appendChild(divInDiv);
247 | footer.firstElementChild.firstElementChild.firstElementChild.appendChild(dlBtnDiv);
248 | }
249 |
250 | function sAddDlBtn(footer) {
251 | const lis = footer.getElementsByTagName('li');
252 | for (const li of lis) {
253 | li.style.width = '25%';
254 | }
255 | let dlBtnLi = document.createElement('li');
256 | dlBtnLi.style.width = '25%';
257 | let aInLi = document.createElement('a');
258 | aInLi.className = 'woo-box-flex woo-box-alignCenter woo-box-justifyCenter';
259 | aInLi.setAttribute('title', '下载');
260 | aInLi.setAttribute('href', 'javascript:void(0);');
261 | let dlBtn = document.createElement('button');
262 | dlBtn.className = 'woo-like-main toolbar_btn download-button';
263 | dlBtn.innerHTML = '下载';
264 | aInLi.addEventListener('click', function (event) {
265 | event.preventDefault();
266 | });
267 | dlBtn.addEventListener('click', function (event) {
268 | event.preventDefault();
269 | const card = this.parentElement.parentElement.parentElement.parentElement;
270 | const cardWrap = card.parentElement;
271 | const mid = cardWrap.getAttribute('mid');
272 | if (mid) {
273 | var response;
274 | if (location.host == 'www.weibo.com') {
275 | response = httpGet('https://www.weibo.com/ajax/statuses/show?id=' + mid);
276 | } else {
277 | response = httpGet('https://weibo.com/ajax/statuses/show?id=' + mid);
278 | }
279 | const resJson = JSON.parse(response);
280 | // console.log(resJson);
281 | let status = resJson;
282 | if (resJson.hasOwnProperty('retweeted_status')) {
283 | status = resJson.retweeted_status;
284 | }
285 | const postId = status.mblogid;
286 | const picInfos = status.pic_infos;
287 | const mixMediaInfo = status.mix_media_info;
288 | const userName = status.user.screen_name;
289 | const userId = status.user.idstr;
290 | const postUid = status.idstr;
291 | const postTime = status.created_at;
292 | const text = status.text_raw;
293 | let downloadList = [];
294 | if (footer.parentElement.getElementsByTagName('video').length > 0) {
295 | // console.log('download video');
296 | if (resJson.hasOwnProperty('page_info')) {
297 | downloadList = downloadList.concat(handleVideo(resJson.page_info.media_info, 1, userName, userId, postId, postUid, 1, postTime, text));
298 | }
299 | }
300 | if (picInfos) {
301 | // console.log('download images');
302 | let index = 0;
303 | let padLength = Object.entries(picInfos).length.toString().length;
304 | for (const [id, pic] of Object.entries(picInfos)) {
305 | index += 1;
306 | downloadList = downloadList.concat(handlePic(pic, padLength, userName, userId, postId, postUid, index, postTime, text));
307 | }
308 | }
309 | if (mixMediaInfo && mixMediaInfo.items) {
310 | // console.log('mix media');
311 | let index = 0;
312 | let padLength = Object.entries(mixMediaInfo.items).length.toString().length;
313 | for (const [id, media] of Object.entries(mixMediaInfo.items)) {
314 | index += 1;
315 | if (media.type === 'video') {
316 | downloadList = downloadList.concat(handleVideo(media.data.media_info, 1, userName, userId, postId, postUid, index, postTime, text));
317 | } else if (media.type === 'pic') {
318 | downloadList = downloadList.concat(handlePic(media.data, padLength, userName, userId, postId, postUid, index, postTime, text));
319 | }
320 | }
321 | }
322 | handleDownloadList(downloadList);
323 | }
324 | });
325 | aInLi.appendChild(dlBtn);
326 | dlBtnLi.appendChild(dlBtn);
327 | footer.firstChild.appendChild(dlBtnLi);
328 | // console.log('added download button');
329 | }
330 |
331 | function bodyMouseOver(event) {
332 | if (location.host == 'weibo.com' || location.host == 'www.weibo.com') {
333 | const footers = document.getElementsByTagName('footer');
334 | for (const footer of footers) {
335 | if (footer.getElementsByClassName('download-button').length > 0) {
336 | } else {
337 | if (footer.parentElement.tagName.toLowerCase() == 'article') {
338 | const article = footer.parentElement;
339 | const imgs = article.getElementsByTagName('img');
340 | let added = false;
341 | if (imgs.length > 0) {
342 | let addFlag = false;
343 | for (const img of imgs) {
344 | if (['woo-picture-img', 'picture_focusImg_1z5In', 'picture-viewer_pic_37YQ3', '_focusImg_a2k8z_23'].includes(img.className)) {
345 | addFlag = true;
346 | }
347 | }
348 | if (addFlag == true) {
349 | addDlBtn(footer);
350 | added = true;
351 | }
352 | }
353 | let videos = article.getElementsByTagName('video');
354 | if (videos.length > 0 && added == false) {
355 | addDlBtn(footer);
356 | }
357 | }
358 | }
359 | }
360 | }
361 | if (location.host == 's.weibo.com') {
362 | const footers = document.querySelectorAll('#pl_feedlist_index .card-act');
363 | for (const footer of footers) {
364 | if (footer.getElementsByClassName('download-button').length > 0) {
365 | } else {
366 | if (footer.parentElement.className == 'card' && footer.parentElement.parentElement.className == 'card-wrap') {
367 | const card = footer.parentElement;
368 | let added = false;
369 | const media_prev = card.querySelector('div[node-type="feed_list_media_prev"]');
370 | if (media_prev) {
371 | const imgs = media_prev.getElementsByTagName('img');
372 | if (imgs.length > 0) {
373 | sAddDlBtn(footer);
374 | added = true;
375 | }
376 | const videos = card.getElementsByTagName('video');
377 | if (videos.length > 0 && added == false) {
378 | sAddDlBtn(footer);
379 | }
380 | }
381 | }
382 | }
383 | }
384 | }
385 | }
386 |
387 | var dlFileName = '{original}.{ext}';
388 |
389 | document.addEventListener('DOMContentLoaded', function () {
390 | // 创建遮罩层
391 | var overlay = document.createElement('div');
392 | overlay.style.display = 'none';
393 | // 设置 overlay 的 id
394 | overlay.setAttribute('id', overlayId);
395 | overlay.classList.add('overlay-' + overlayId);
396 | // 创建加载框
397 | var loadingBox = document.createElement('div');
398 | loadingBox.classList.add('loadingBox-' + overlayId);
399 | // 创建加载动画元素
400 | var loadingAnimation = document.createElement('div');
401 | loadingAnimation.classList.add('loading-animation-' + overlayId);
402 | // 将加载动画元素添加到加载框
403 | loadingBox.appendChild(loadingAnimation);
404 | // 将加载框添加到遮罩层
405 | overlay.appendChild(loadingBox);
406 | // 将遮罩层添加到 body
407 | document.body.appendChild(overlay);
408 | // 添加样式
409 | var style = document.createElement('style');
410 | style.textContent = `
411 | .overlay-${overlayId} {
412 | position: fixed;
413 | top: 0;
414 | left: 0;
415 | width: 100%;
416 | height: 100%;
417 | background-color: rgba(0, 0, 0, 0.5);
418 | justify-content: center;
419 | align-items: center;
420 | z-index: 9999;
421 | }
422 |
423 | .loadingBox-${overlayId} {
424 | display: flex;
425 | flex-direction: column;
426 | align-items: center;
427 | }
428 |
429 | .loading-animation-${overlayId} {
430 | width: 50px;
431 | height: 50px;
432 | border: 5px solid #fff;
433 | border-radius: 50%;
434 | border-top: 5px solid #3498db;
435 | animation: spin 1s linear infinite;
436 | }
437 |
438 | @keyframes spin {
439 | 0% { transform: rotate(0deg); }
440 | 100% { transform: rotate(360deg); }
441 | }
442 | `;
443 | document.head.appendChild(style);
444 |
445 | // console.log('我被执行了!');
446 | document.body.addEventListener('mouseover', bodyMouseOver);
447 | });
448 |
449 |
--------------------------------------------------------------------------------