// ==UserScript== // @author original author T3rry, modified by JackSlow // @name 115 mediainfo fetcher // @description 115网盘在线获取mediainfo // @namespace https://115.com/MediaInfo // @version 3.1.7 // @match https://115.com/* // @grant GM_xmlhttpRequest // @grant unsafeWindow // @grant GM_log // @connect proapi.115.com // @connect 115.com // @require https://cdn.jsdelivr.net/npm/node-forge@0.10.0/dist/forge.min.js // @require https://unpkg.com/mediainfo.js/dist/mediainfo.min.js // @downloadURL none // ==/UserScript== ;(function () { 'use strict' const pub_key = `-----BEGIN PUBLIC KEY-----\ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDR3rWmeYnRClwLBB0Rq0dlm8Mr\ PmWpL5I23SzCFAoNpJX6Dn74dfb6y02YH15eO6XmeBHdc7ekEFJUIi+swganTokR\ IVRRr/z16/3oh7ya22dcAqg191y+d6YDr4IGg/Q5587UKJMj35yQVXaeFXmLlFPo\ kFiz4uPxhrB7BGqZbQIDAQAB\ -----END PUBLIC KEY-----` const private_key = `-----BEGIN RSA PRIVATE KEY-----\ MIICXAIBAAKBgQCMgUJLwWb0kYdW6feyLvqgNHmwgeYYlocst8UckQ1+waTOKHFC\ TVyRSb1eCKJZWaGa08mB5lEu/asruNo/HjFcKUvRF6n7nYzo5jO0li4IfGKdxso6\ FJIUtAke8rA2PLOubH7nAjd/BV7TzZP2w0IlanZVS76n8gNDe75l8tonQQIDAQAB\ AoGANwTasA2Awl5GT/t4WhbZX2iNClgjgRdYwWMI1aHbVfqADZZ6m0rt55qng63/\ 3NsjVByAuNQ2kB8XKxzMoZCyJNvnd78YuW3Zowqs6HgDUHk6T5CmRad0fvaVYi6t\ viOkxtiPIuh4QrQ7NUhsLRtbH6d9s1KLCRDKhO23pGr9vtECQQDpjKYssF+kq9iy\ A9WvXRjbY9+ca27YfarD9WVzWS2rFg8MsCbvCo9ebXcmju44QhCghQFIVXuebQ7Q\ pydvqF0lAkEAmgLnib1XonYOxjVJM2jqy5zEGe6vzg8aSwKCYec14iiJKmEYcP4z\ DSRms43hnQsp8M2ynjnsYCjyiegg+AZ87QJANuwwmAnSNDOFfjeQpPDLy6wtBeft\ 5VOIORUYiovKRZWmbGFwhn6BQL+VaafrNaezqUweBRi1PYiAF2l3yLZbUQJAf/nN\ 4Hz/pzYmzLlWnGugP5WCtnHKkJWoKZBqO2RfOBCq+hY4sxvn3BHVbXqGcXLnZPvo\ YuaK7tTXxZSoYLEzeQJBAL8Mt3AkF1Gci5HOug6jT4s4Z+qDDrUXo9BlTwSWP90v\ wlHF+mkTJpKd5Wacef0vV+xumqNorvLpIXWKwxNaoHM=\ -----END RSA PRIVATE KEY-----` const priv = window.forge.pki.privateKeyFromPem(private_key) const pub = window.forge.pki.publicKeyFromPem(pub_key) const g_key_l = [0x42, 0xda, 0x13, 0xba, 0x78, 0x76, 0x8d, 0x37, 0xe8, 0xee, 0x04, 0x91] const g_key_s = [0x29, 0x23, 0x21, 0x5e] const g_kts = [0xf0, 0xe5, 0x69, 0xae, 0xbf, 0xdc, 0xbf, 0x5a, 0x1a, 0x45, 0xe8, 0xbe, 0x7d, 0xa6, 0x73, 0x88, 0xde, 0x8f, 0xe7, 0xc4, 0x45, 0xda, 0x86, 0x94, 0x9b, 0x69, 0x92, 0x0b, 0x6a, 0xb8, 0xf1, 0x7a, 0x38, 0x06, 0x3c, 0x95, 0x26, 0x6d, 0x2c, 0x56, 0x00, 0x70, 0x56, 0x9c, 0x36, 0x38, 0x62, 0x76, 0x2f, 0x9b, 0x5f, 0x0f, 0xf2, 0xfe, 0xfd, 0x2d, 0x70, 0x9c, 0x86, 0x44, 0x8f, 0x3d, 0x14, 0x27, 0x71, 0x93, 0x8a, 0xe4, 0x0e, 0xc1, 0x48, 0xae, 0xdc, 0x34, 0x7f, 0xcf, 0xfe, 0xb2, 0x7f, 0xf6, 0x55, 0x9a, 0x46, 0xc8, 0xeb, 0x37, 0x77, 0xa4, 0xe0, 0x6b, 0x72, 0x93, 0x7e, 0x51, 0xcb, 0xf1, 0x37, 0xef, 0xad, 0x2a, 0xde, 0xee, 0xf9, 0xc9, 0x39, 0x6b, 0x32, 0xa1, 0xba, 0x35, 0xb1, 0xb8, 0xbe, 0xda, 0x78, 0x73, 0xf8, 0x20, 0xd5, 0x27, 0x04, 0x5a, 0x6f, 0xfd, 0x5e, 0x72, 0x39, 0xcf, 0x3b, 0x9c, 0x2b, 0x57, 0x5c, 0xf9, 0x7c, 0x4b, 0x7b, 0xd2, 0x12, 0x66, 0xcc, 0x77, 0x09, 0xa6] const m115_l_rnd_key = genRandom(16) let m115_s_rnd_key = [] let key_s = [] let key_l = [] function intToByte (i) { var b = i & 0xFF var c = 0 if (b >= 256) { c = b % 256 c = -1 * (256 - c) } else { c = b } return c } function stringToArray (s) { var map = Array.prototype.map var array = map.call(s, function (x) { return x.charCodeAt(0) }) return array } function arrayTostring (array) { var result = '' for (var i = 0; i < array.length; ++i) { result += (String.fromCharCode(array[i])) } return result } function m115_init () { key_s = [] key_l = [] } function m115_setkey (randkey, sk_len) { var length = sk_len * (sk_len - 1) var index = 0 var xorkey = '' if (randkey) { for (var i = 0; i < sk_len; i++) { var x = intToByte((randkey[i]) + (g_kts[index])) xorkey += String.fromCharCode(g_kts[length] ^ x) length -= sk_len index += sk_len } if (sk_len === 4) { key_s = stringToArray(xorkey) } else if (sk_len === 12) { key_l = stringToArray(xorkey) } } } function xor115_enc (src, key) { var lkey = key.length var secret = [] var num = 0 var pad = (src.length) % 4 if (pad > 0) { for (var i = 0; i < pad; i++) { secret.push((src[i]) ^ key[i]) } src = src.slice(pad) } for (i = 0; i < src.length; i++) { if (num >= lkey) { num = num % lkey } secret.push((src[i] ^ key[num])) num += 1 } return secret } function genRandom (len) { var keys = [] var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz23456789' var maxPos = chars.length for (var i = 0; i < len; i++) { keys.push(chars.charAt(Math.floor(Math.random() * maxPos)).charCodeAt(0)) } return keys } function m115_encode (plaintext) { // console.log('m115_encode:') m115_init() key_l = g_key_l m115_setkey(m115_l_rnd_key, 4) var tmp = xor115_enc(stringToArray(plaintext), key_s).reverse() var xortext = xor115_enc(tmp, key_l) var text = arrayTostring(m115_l_rnd_key) + arrayTostring(xortext) var ciphertext = pub.encrypt(text) ciphertext = encodeURIComponent(window.forge.util.encode64(ciphertext)) return ciphertext } function m115_decode (ciphertext) { // console.log('m115_decode:') var bciphertext = window.forge.util.decode64(ciphertext) var block = bciphertext.length / (128) var plaintext = '' var index = 0 for (var i = 1; i <= block; ++i) { plaintext += priv.decrypt(bciphertext.slice(index, i * 128)) index += 128 } m115_s_rnd_key = stringToArray(plaintext.slice(0, 16)) plaintext = plaintext.slice(16) m115_setkey(m115_l_rnd_key, 4) m115_setkey(m115_s_rnd_key, 12) var tmp = xor115_enc(stringToArray(plaintext), key_l).reverse() plaintext = xor115_enc(tmp, key_s) return arrayTostring(plaintext) } function PostData (dict) { var k, tmp, v tmp = [] for (k in dict) { v = dict[k] tmp.push(k + '=' + v) } // console.log(tmp.join('&')) return tmp.join('&') }; waitForKeyElements('div.file-opr', AddMediaInfoBtn) function AddMediaInfoBtn (jNode) { var aclass2 = document.createElement('a') aclass2.addEventListener('click', function (e) { ispan2.innerText = '获取中...' handleMediaInfoButton(jNode.parentNode).then(() => ispan2.innerText = '获取MediaInfo') }) var iclass2 = document.createElement('i') var ispan2 = document.createElement('span') var node2 = document.createTextNode('获取MediaInfo') ispan2.appendChild(node2) aclass2.appendChild(iclass2) aclass2.appendChild(ispan2) jNode.appendChild(aclass2) } function getFileUri ({ file_id, pick_code }) { return new Promise((resolve, reject) => { const data = PostData({ data: m115_encode(`{"pickcode":"${pick_code}"}`) }) // console.log('PostData:', data) GM_xmlhttpRequest({ data, method: 'POST', url: 'https://proapi.115.com/app/chrome/downurl', headers: { 'Content-Type': 'application/x-www-form-urlencoded', "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", }, responseType: 'json', onerror: reject, onabort: reject, onload: function (response) { if (response.status === 200) { let json = m115_decode(response.response.data) json = JSON.parse(json) // console.log('GetFileLink response json:', json) const fileUri = json[file_id]['url']['url'] // const cookie = DeleteCookie(response.responseHeaders) || null // resolve({fileUri, cookie}) // no need cookies to download file resolve(fileUri) } else { console.log('getFileUri response:', response) reject(new Error('获取文件直链失败')) } } }) }) } function fetchChunk ({ fileUri, headers }) { headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36' return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: fileUri, headers: headers, responseType: 'arraybuffer', onerror: reject, onabort: reject, onload: function (response) { if ([206, 200].includes(response.status)) { resolve(new Uint8Array(response.response)) } else { console.log('fetchChunk response:', response) reject(new Error('获取文件内容失败')) } } }) }) } function readChunk ({ totalSize, fileUri, size, offset }) { if (!size) return new Uint8Array([]) const headers = {} const start = offset const end = Math.min(offset + size, totalSize) // console.log('readChunk:', {start, end}) if (Number.isInteger(start) && Number.isInteger(end)) headers.Range = `bytes=${start}-${end}` return fetchChunk({ fileUri, headers }) } async function handleMediaInfoButton (liNode) { const file_type = liNode.getAttribute('file_type') if (file_type !== '1') return alert('请选择文件') const totalSize = parseInt(liNode.getAttribute('file_size')) if (!totalSize) return alert('文件大小缺失') const sha1 = liNode.getAttribute('sha1') // if (!sha1) return alert('文件sha1缺失') const file_id = liNode.getAttribute('file_id') if (!file_id) return alert('文件file_id缺失') if (miRunning[file_id]) return alert('正在获取此文件的mediainfo,请稍后') const pick_code = liNode.getAttribute('pick_code') if (!pick_code) return alert('文件pick_code缺失') const file_name = liNode.getAttribute('title') // console.log({file_name, totalSize, file_type, sha1, file_id}) miRunning[file_id] = true try { const fileUri = await getFileUri({ pick_code, file_id }) const info = await getMediainfo({ totalSize, fileUri, file_id, sha1 }) showMediainfo(file_name, info) } catch (e) { console.log('getMediainfo error:', e) alert('获取mediainfo失败:' + e.message) } miRunning[file_id] = false } function showMediainfo (filename, info) { const head = `FileName: ${filename}\n\n==== MediaInfo ====\n\n` const result = head + info const win = window.open('', filename, 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=800,height=900') win.document.body.innerHTML = `
${result}
` } const miCache = {} const miRunning = {} async function getMediainfo ({ totalSize, fileUri, file_id, sha1 }) { const exist = miCache[sha1] || miCache[file_id] if (exist) return exist const MI = window.MediaInfo if (!MI) throw new Error('MEDIAINFO依赖脚本未加载') const mediainfo = MI && await MI({ format: 'text', chunkSize: 1024 * 1024 * 3, locateFile: () => 'https://unpkg.com/mediainfo.js/dist/MediaInfoModule.wasm' }) const info = await mediainfo.analyzeData(() => totalSize, (size, offset) => { return readChunk({ totalSize, fileUri, size, offset }) }) return miCache[file_id] = miCache[sha1] = info } // https://gist.github.com/mjblay/18d34d861e981b7785e407c3b443b99b /* --- waitForKeyElements(): A utility function, for Greasemonkey scripts, that detects and handles AJAXed content. Forked for use without JQuery. Usage example: waitForKeyElements ( "div.comments" , commentCallbackFunction ); //--- Page-specific function to do what we want when the node is found. function commentCallbackFunction (element) { element.text ("This comment changed by waitForKeyElements()."); } IMPORTANT: Without JQuery, this fork does not look into the content of iframes. */ function waitForKeyElements ( selectorTxt, /* Required: The selector string that specifies the desired element(s). */ actionFunction, /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */ bWaitOnce /* Optional: If false, will continue to scan for new elements even after the first match is found. */ ) { var targetNodes, btargetsFound targetNodes = document.querySelectorAll(selectorTxt) if (targetNodes && targetNodes.length > 0) { btargetsFound = true /* --- Found target node(s). Go through each and act if they are new. */ targetNodes.forEach(function (element) { var alreadyFound = element.dataset.found === 'alreadyFound' ? 'alreadyFound' : false if (!alreadyFound) { // --- Call the payload function. var cancelFound = actionFunction(element) if (cancelFound) { btargetsFound = false } else { element.dataset.found = 'alreadyFound' } } }) } else { btargetsFound = false } // --- Get the timer-control variable for this selector. var controlObj = waitForKeyElements.controlObj || {} var controlKey = selectorTxt.replace(/[^\w]/g, '_') var timeControl = controlObj[controlKey] // --- Now set or clear the timer as appropriate. if (btargetsFound && bWaitOnce && timeControl) { // --- The only condition where we need to clear the timer. clearInterval(timeControl) delete controlObj[controlKey] } else { // --- Set a timer, if needed. if (!timeControl) { timeControl = setInterval(function () { waitForKeyElements(selectorTxt, actionFunction, bWaitOnce ) }, 300 ) controlObj[controlKey] = timeControl } } waitForKeyElements.controlObj = controlObj } })()