// ==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/[email protected]/dist/forge.min.js
// @require https://unpkg.com/mediainfo.js/dist/mediainfo.min.js
// ==/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 = `<pre style="white-space: pre-wrap;">${result}</pre>`
}
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
}
})()