├── 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 | ![使用方法](doc/use.png) 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 | --------------------------------------------------------------------------------