├── screenshots ├── aria2.gif └── paste-detect.gif ├── .editorconfig ├── README.md ├── LICENSE └── weiyun.user.js /screenshots/aria2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo2k/WeiyunHelper/HEAD/screenshots/aria2.gif -------------------------------------------------------------------------------- /screenshots/paste-detect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo2k/WeiyunHelper/HEAD/screenshots/paste-detect.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeiyunHelper 2 | 3 | WeiyunHelper 是微云的辅助脚本,拥有以下功能: 4 | 5 | - [x] 🔗 下载文件时支持通过 AriaNg 下载 6 | - [x] 🧲 支持粘贴自动(快捷)下载磁力链链接 7 | - [x] 🎊 同时支持个人文件管理页和分享页的 AriaNG 下载 8 | - [ ] 💡 你有什么[想法](https://github.com/loo2k/WeiyunHelper/issues)? 9 | 10 | ## 如何使用 11 | 12 | 使用前需要先安装 [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) 扩展,安装完成后访问下方的安装地址: 13 | 14 | 👉🏼 [https://greasyfork.org/zh-CN/scripts/402669-weiyunhelper](https://greasyfork.org/zh-CN/scripts/402669-weiyunhelper) 15 | 16 | ### 配置 AriaNg 17 | 18 | 因为目前没有直接调用 Aria2 的接口,依赖了 AriaNg 的服务。可以在 [http://ariang.mayswind.net/latest/](http://ariang.mayswind.net/latest/) 进行对应的设置后使用。 19 | 20 | 备注: 21 | - 用户可以自行修改代码中的 AriaNg 服务地址 22 | - 如果你希望支持直接调用 Aria2 的接口也可以发起 PR 贡献你的代码 :) 23 | 24 | ## 功能概览 25 | 26 | **🔗 下载文件时支持通过 AriaNg 下载** 27 | 28 | ![aria2](./screenshots/aria2.gif) 29 | 30 | **🧲 支持粘贴自动(快捷)下载磁力链链接** 31 | 32 | ![paste](./screenshots/paste-detect.gif) 33 | 34 | ## 声明 35 | 36 | WeiyunHelper 仅供个人学习交流,严禁用于商业用途。 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /weiyun.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name WeiyunHelper - 微云 Aria2 下载辅助脚本 3 | // @namespace https://github.com/loo2k/WeiyunHelper/ 4 | // @version 0.0.9 5 | // @description 微云下载时文件支持导出到 aria2 下载,支持分享页面及个人云盘管理页 6 | // @author Luke 7 | // @match *://*.weiyun.com/* 8 | // @grant none 9 | // @run-at document-end 10 | // @updateURL https://github.com/loo2k/WeiyunHelper/raw/master/weiyun.user.js 11 | // @downloadURL https://github.com/loo2k/WeiyunHelper/raw/master/weiyun.user.js 12 | // @supportURL https://github.com/loo2k/WeiyunHelper/issues 13 | // ==/UserScript== 14 | 15 | (function () { 16 | 'use strict'; 17 | 18 | var B64 = { 19 | alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', 20 | lookup: null, 21 | ie: /MSIE /.test(navigator.userAgent), 22 | ieo: /MSIE [67]/.test(navigator.userAgent), 23 | encode: function (s) { 24 | /* jshint bitwise:false */ 25 | var buffer = B64.toUtf8(s), 26 | position = -1, 27 | result, 28 | len = buffer.length, 29 | nan0, nan1, nan2, enc = [, , , ]; 30 | 31 | if (B64.ie) { 32 | result = []; 33 | while (++position < len) { 34 | nan0 = buffer[position]; 35 | nan1 = buffer[++position]; 36 | enc[0] = nan0 >> 2; 37 | enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4); 38 | if (isNaN(nan1)) 39 | enc[2] = enc[3] = 64; 40 | else { 41 | nan2 = buffer[++position]; 42 | enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6); 43 | enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63; 44 | } 45 | result.push(B64.alphabet.charAt(enc[0]), B64.alphabet.charAt(enc[1]), B64.alphabet.charAt(enc[2]), B64.alphabet.charAt(enc[3])); 46 | } 47 | return result.join(''); 48 | } else { 49 | result = ''; 50 | while (++position < len) { 51 | nan0 = buffer[position]; 52 | nan1 = buffer[++position]; 53 | enc[0] = nan0 >> 2; 54 | enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4); 55 | if (isNaN(nan1)) 56 | enc[2] = enc[3] = 64; 57 | else { 58 | nan2 = buffer[++position]; 59 | enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6); 60 | enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63; 61 | } 62 | result += B64.alphabet[enc[0]] + B64.alphabet[enc[1]] + B64.alphabet[enc[2]] + B64.alphabet[enc[3]]; 63 | } 64 | return result; 65 | } 66 | }, 67 | decode: function (s) { 68 | /* jshint bitwise:false */ 69 | s = s.replace(/\s/g, ''); 70 | if (s.length % 4) 71 | throw new Error('InvalidLengthError: decode failed: The string to be decoded is not the correct length for a base64 encoded string.'); 72 | if(/[^A-Za-z0-9+\/=\s]/g.test(s)) 73 | throw new Error('InvalidCharacterError: decode failed: The string contains characters invalid in a base64 encoded string.'); 74 | 75 | var buffer = B64.fromUtf8(s), 76 | position = 0, 77 | result, 78 | len = buffer.length; 79 | 80 | if (B64.ieo) { 81 | result = []; 82 | while (position < len) { 83 | if (buffer[position] < 128) 84 | result.push(String.fromCharCode(buffer[position++])); 85 | else if (buffer[position] > 191 && buffer[position] < 224) 86 | result.push(String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63))); 87 | else 88 | result.push(String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63))); 89 | } 90 | return result.join(''); 91 | } else { 92 | result = ''; 93 | while (position < len) { 94 | if (buffer[position] < 128) 95 | result += String.fromCharCode(buffer[position++]); 96 | else if (buffer[position] > 191 && buffer[position] < 224) 97 | result += String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63)); 98 | else 99 | result += String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63)); 100 | } 101 | return result; 102 | } 103 | }, 104 | toUtf8: function (s) { 105 | /* jshint bitwise:false */ 106 | var position = -1, 107 | len = s.length, 108 | chr, buffer = []; 109 | if (/^[\x00-\x7f]*$/.test(s)) while (++position < len) 110 | buffer.push(s.charCodeAt(position)); 111 | else while (++position < len) { 112 | chr = s.charCodeAt(position); 113 | if (chr < 128) 114 | buffer.push(chr); 115 | else if (chr < 2048) 116 | buffer.push((chr >> 6) | 192, (chr & 63) | 128); 117 | else 118 | buffer.push((chr >> 12) | 224, ((chr >> 6) & 63) | 128, (chr & 63) | 128); 119 | } 120 | return buffer; 121 | }, 122 | fromUtf8: function (s) { 123 | /* jshint bitwise:false */ 124 | var position = -1, 125 | len, buffer = [], 126 | enc = [, , , ]; 127 | if (!B64.lookup) { 128 | len = B64.alphabet.length; 129 | B64.lookup = {}; 130 | while (++position < len) 131 | B64.lookup[B64.alphabet.charAt(position)] = position; 132 | position = -1; 133 | } 134 | len = s.length; 135 | while (++position < len) { 136 | enc[0] = B64.lookup[s.charAt(position)]; 137 | enc[1] = B64.lookup[s.charAt(++position)]; 138 | buffer.push((enc[0] << 2) | (enc[1] >> 4)); 139 | enc[2] = B64.lookup[s.charAt(++position)]; 140 | if (enc[2] === 64) 141 | break; 142 | buffer.push(((enc[1] & 15) << 4) | (enc[2] >> 2)); 143 | enc[3] = B64.lookup[s.charAt(++position)]; 144 | if (enc[3] === 64) 145 | break; 146 | buffer.push(((enc[2] & 3) << 6) | enc[3]); 147 | } 148 | return buffer; 149 | } 150 | }; 151 | 152 | var B64url = { 153 | decode: function(input) { 154 | // Replace non-url compatible chars with base64 standard chars 155 | input = input 156 | .replace(/-/g, '+') 157 | .replace(/_/g, '/'); 158 | 159 | // Pad out with standard base64 required padding characters 160 | var pad = input.length % 4; 161 | if(pad) { 162 | if(pad === 1) { 163 | throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); 164 | } 165 | input += new Array(5-pad).join('='); 166 | } 167 | 168 | return B64.decode(input); 169 | }, 170 | 171 | encode: function(input) { 172 | var output = B64.encode(input); 173 | return output 174 | .replace(/\+/g, '-') 175 | .replace(/\//g, '_') 176 | .split('=', 1)[0]; 177 | } 178 | }; 179 | 180 | const base64 = { 181 | decode: B64.decode, 182 | encode: B64.encode, 183 | urldecode: B64url.decode, 184 | urlencode: B64url.encode, 185 | }; 186 | 187 | Date.prototype.Format = function (fmt) { 188 | var o = { 189 | 'M+': this.getMonth() + 1, 190 | 'd+': this.getDate(), 191 | 'h+': this.getHours(), 192 | 'm+': this.getMinutes(), 193 | 's+': this.getSeconds(), 194 | 'q+': Math.floor((this.getMonth() + 3) / 3), 195 | S: this.getMilliseconds(), 196 | }; 197 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)); 198 | for (var k in o) 199 | if (new RegExp('(' + k + ')').test(fmt)) 200 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)); 201 | return fmt; 202 | }; 203 | 204 | /** 205 | * 将微云返回的下载地址解析到 Aria2 进行下载 206 | * 207 | * @param {void} 208 | */ 209 | const handleResp2Aria2 = (ret) => { 210 | let downloadUrl = ''; 211 | let cookieName = ''; 212 | let cookieValue = ''; 213 | let URI = {}; 214 | let fileName = ''; 215 | if (ret.file_list) { 216 | downloadUrl = ret.file_list[0].https_download_url; 217 | cookieName = ret.file_list[0].cookie_name; 218 | cookieValue = ret.file_list[0].cookie_value; 219 | URI = new URL(downloadUrl); 220 | fileName = decodeURI(URI.pathname.substr(URI.pathname.lastIndexOf('/') + 1)); 221 | } else { 222 | downloadUrl = ret.https_download_url; 223 | cookieName = ret.cookie_name; 224 | cookieValue = ret.cookie_value; 225 | fileName = `微云合并下载文件_${new Date().Format('yyyy-MM-dd hh:mm:ss')}.zip`; 226 | } 227 | 228 | const ariaNgUrl = `http://ariang.mayswind.net/latest/#!/new/task?url=${base64.urlencode(downloadUrl)}&header=Cookie:${cookieName}=${cookieValue}&out=${encodeURI(fileName)}`; 229 | 230 | console.log('文件名称:', fileName); 231 | console.log('下载地址:', downloadUrl); 232 | console.log('请求参数:', `Cookie:${cookieName}=${cookieValue}`); 233 | console.log('AriaNg URL:', ariaNgUrl); 234 | 235 | // 使用 ariaNg 进行下载 236 | window.open(ariaNgUrl); 237 | } 238 | 239 | const injectChunkId = Math.random().toString(36).substring(7); 240 | 241 | // 微云文件分享页面注入脚本模块 242 | location.host === 'share.weiyun.com' && webpackJsonp([7892], {[injectChunkId]: function(modules, exports, require) { 243 | // 寻找 DownloadRequest 模块 244 | const [ DownloadRequest ] = Object.values(require.c) 245 | .filter((x) => x.exports && typeof x.exports.DownloadRequest === 'function' && typeof x.exports.DownloadType === 'object') 246 | .map((x) => x.exports.DownloadRequest); 247 | 248 | // 寻找 DownloadOperate 模块 249 | const [ DownloadOperate ] = Object.values(require.c) 250 | .filter((x) => x.exports && typeof x.exports.DownloadOperate === 'function') 251 | .map((x) => x.exports.DownloadOperate); 252 | 253 | // 获取 Vue 应用实例 254 | const $Vue = document.getElementById('app').__vue__; 255 | 256 | // 判断依赖模块是否存在 257 | if (!DownloadRequest || !DownloadOperate) { 258 | console.error('没有检测到适配模块,已退出 WeiyunHelper'); 259 | console.error('你可以到 https://github.com/loo2k/WeiyunHelper/issues 向作者反馈问题') 260 | return false; 261 | } 262 | 263 | // 下载选中文件 264 | function downloadSelectedFiles() { 265 | const { shareFile } = $Vue.$store.state.sharefile; 266 | if (!$Vue.$store.getters["sharefile/isSelected"]) { 267 | if (shareFile.shareFile.childNodes.length === 1) { 268 | shareFile.shareFile.selectAllFiles(); 269 | } else { 270 | return alert('你都还没有选择文件 :('); 271 | } 272 | } 273 | 274 | const downloadOptions = { 275 | fileOwner: shareFile.shareOwner, 276 | shareKey: shareFile.shareKey, 277 | sharePwd: shareFile.sharePwd, 278 | downloadType: 0 279 | } 280 | 281 | return new DownloadOperate(shareFile.shareFile, downloadOptions) 282 | .downloadWithType_(new DownloadRequest(), downloadOptions) 283 | .then(handleResp2Aria2).catch(e => { alert(e.msg) }); 284 | } 285 | 286 | // 监听 body 的 DOM 变化并将下载入口植入 287 | const observeTarget = document.body; 288 | const observeConfig = { attributes: true, childList: true, subtree: true }; 289 | const observeCallback = function (mutations, observer) { 290 | for (let mutation of mutations) { 291 | if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 292 | mutation.addedNodes.forEach((node) => { 293 | // 判断页面中增加的元素是否是针对文件的下拉菜单 294 | if ( 295 | node.className && 296 | node.className.indexOf('mod-bubble-context-menu') > -1 && 297 | node.__vue__ && 298 | node.__vue__.items.some(e => e.method === 'download') 299 | ) { 300 | const contextItems = node.querySelectorAll('.menu-item'); 301 | const newContextItem = document.createElement('li') 302 | newContextItem.className = 'menu-item'; 303 | newContextItem.innerHTML = '使用 Aria 下载'; 304 | newContextItem.addEventListener('click', function() { 305 | downloadSelectedFiles(); 306 | // 关闭右键菜单 307 | document.dispatchEvent(new Event('mousedown')); 308 | }); 309 | contextItems[0].parentNode.insertBefore(newContextItem, contextItems[0].nextSibling); 310 | } 311 | }) 312 | } 313 | } 314 | } 315 | const observeInstance = new MutationObserver(observeCallback); 316 | observeInstance.observe(observeTarget, observeConfig); 317 | 318 | // 直接注入工具条的下载入口 319 | const actionWrapCode = document.querySelector('.mod-action-wrap-code'); 320 | const actionWrapAria = document.createElement('div'); 321 | actionWrapAria.className = 'mod-action-wrap mod-action-wrap-menu mod-action-wrap-aria clearfix'; 322 | 323 | const actionItem = document.createElement('div'); 324 | actionItem.className = 'action-item'; 325 | actionItem.innerHTML = '
使用 Aria 下载
'; 326 | actionItem.addEventListener('click', function () { 327 | downloadSelectedFiles(); 328 | }); 329 | actionWrapAria.appendChild(actionItem); 330 | actionWrapCode.parentNode.insertBefore(actionWrapAria, actionWrapCode); 331 | }}, [injectChunkId]); 332 | 333 | // 微云云盘管理页面注入脚本模块 334 | location.host === 'www.weiyun.com' && webpackJsonp([7891], {[injectChunkId]: function(modules, exports, require) { 335 | // 寻找云盘操作 API 模块 336 | const diskServices = Object.values(require.c) 337 | .filter((x) => x.exports && typeof x.exports.namespace === 'function' && typeof x.exports.namespace('PERSON').fetchUserInfo === 'function') 338 | .map((x) => x.exports.namespace); 339 | const diskService = diskServices && diskServices[0]('PERSON'); 340 | 341 | if (diskServices.length === 0) { 342 | console.error('没有检测到适配模块,已退出 WeiyunHelper'); 343 | console.error('你可以到 https://github.com/loo2k/WeiyunHelper/issues 向作者反馈问题') 344 | return false; 345 | } 346 | 347 | // 下载选中的文件 348 | function downloadSelectedFiles() { 349 | let request = null; 350 | const selected = document.querySelectorAll('.list-group-item.checked.act'); 351 | const fileNodes = Array.from(selected).map(item => item.__vue__.fileNode); 352 | if (fileNodes.length === 1 && !fileNodes[0].isDir()) { 353 | request = diskService.fetchDownloadFileInfo({ fileNodes }); 354 | } else { 355 | request = diskService.fetchPackDownloadDirFileInfo({ fileNodes }); 356 | } 357 | 358 | request.then(handleResp2Aria2).catch(e => { alert(e.msg) }); 359 | } 360 | 361 | // 监听 body 的 DOM 变化并将下载入口植入 362 | const observeTarget = document.body; 363 | const observeConfig = { attributes: true, childList: true, subtree: true }; 364 | const observeCallback = function (mutations, observer) { 365 | for (let mutation of mutations) { 366 | if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 367 | mutation.addedNodes.forEach((node) => { 368 | // 判断页面中增加的元素是否是针对文件的下拉菜单 369 | if ( 370 | node.className && 371 | node.className.indexOf('mod-bubble-context-menu') > -1 && 372 | node.__vue__ && 373 | node.__vue__.items.some(e => e.method === 'download') 374 | ) { 375 | const contextItems = node.querySelectorAll('.menu-item'); 376 | const newContextItem = document.createElement('li') 377 | newContextItem.className = 'menu-item'; 378 | newContextItem.innerHTML = '使用 Aria 下载'; 379 | newContextItem.addEventListener('click', function() { 380 | downloadSelectedFiles(); 381 | // 关闭右键菜单 382 | document.dispatchEvent(new Event('mousedown')); 383 | }); 384 | contextItems[0].parentNode.insertBefore(newContextItem, contextItems[0].nextSibling); 385 | } 386 | }) 387 | } 388 | 389 | // 针对顶部下载菜单 390 | if ( 391 | mutation.type === 'attributes' && 392 | mutation.attributeName === 'style' && 393 | mutation.target.className.indexOf('mod-action-wrap-menu') > -1 && 394 | mutation.target.style.display !== 'none' && 395 | mutation.target.querySelectorAll('#action-item-aria').length === 0 396 | ) { 397 | const actionItems = mutation.target.querySelectorAll('.action-item'); 398 | const newActionItem = document.createElement('div'); 399 | newActionItem.id = 'action-item-aria' 400 | newActionItem.className = 'action-item'; 401 | newActionItem.innerHTML = '
使用 Aria 下载
'; 402 | newActionItem.addEventListener('click', function () { 403 | downloadSelectedFiles(); 404 | }); 405 | mutation.target.insertBefore(newActionItem, actionItems[0].nextSibling); 406 | } 407 | } 408 | } 409 | const observeInstance = new MutationObserver(observeCallback); 410 | observeInstance.observe(observeTarget, observeConfig); 411 | 412 | // 打开离线下载窗口并填写链接 413 | const openDownloadModal = (text = '') => { 414 | let wyCreateBtn = document.querySelectorAll('.mod-action-wrap-create'); 415 | let $wyCreateBtn = wyCreateBtn[0] && wyCreateBtn[0].__vue__; 416 | $wyCreateBtn.offlineDownload(); 417 | 418 | // 点击使用磁力链下载 419 | let modalBtNav = document.querySelectorAll('.modal-dialog-bt .modal-tab-nav .tab-nav-item'); 420 | modalBtNav.forEach(nav => { 421 | if (nav.innerText.trim() === '链接下载') { 422 | nav.click(); 423 | } 424 | }); 425 | 426 | setTimeout(() => { 427 | // 填写 magent 或者 ed2k 链接 428 | let urlTextarea = document.querySelector('.modal-dialog-bt .tab-cont-item.online .input-block'); 429 | if (text) { 430 | urlTextarea.value = text; 431 | urlTextarea.dispatchEvent(new Event('input')); 432 | } 433 | }, 0); 434 | } 435 | 436 | // 粘贴磁力链或者 ed2k 时自动启动下载 437 | document.addEventListener('paste', (event) => { 438 | // 针对非输入框的粘贴时间 439 | if (['TEXTAREA', 'INPUT'].includes(event.target.tagName)) { 440 | return; 441 | } 442 | 443 | // 剪切板数据对象 444 | let clipboardData = event.clipboardData || window.clipboardData; 445 | 446 | // 剪切板对象可以获取 447 | if (!clipboardData) { return; } 448 | 449 | let paste = clipboardData.getData('text'); 450 | let isEd2k = /^ed2k:\/\//ig.test(paste); 451 | let isMagent = /^magnet:/ig.test(paste); 452 | if (isEd2k || isMagent) { 453 | openDownloadModal(paste); 454 | } 455 | }); 456 | }}, [injectChunkId]); 457 | })(); 458 | --------------------------------------------------------------------------------