115 mediainfo fetcher

115网盘在线获取mediainfo

// ==UserScript==
// @author       original author T3rry, modified by JackSlow, new key generated by showmethemoney2022
// @name         115 mediainfo fetcher
// @description  115网盘在线获取mediainfo
// @namespace    https://115.com/MediaInfo
// @version      3.1.9
// @match        https://115.com/*
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        GM_log
// @connect      proapi.115.com
// @connect      115.com
// @require      https://unpkg.com/node-forge@0.10.0/dist/forge.min.js
// @require      https://unpkg.com/mediainfo.js@0.1.5/dist/mediainfo.js
// @require      https://unpkg.com/big-integer@1.6.51/BigInteger.min.js
// ==/UserScript==

;
(function() {
    'use strict'

    class MyRsa {
        constructor() {
            this.n = bigInt('8686980c0f5a24c4b9d43020cd2c22703ff3f450756529058b1cf88f09b8602136477198a6e2683149659bd122c33592fdb5ad47944ad1ea4d36c6b172aad6338c3bb6ac6227502d010993ac967d1aef00f0c8e038de2e4d3bc2ec368af2e9f10a6f1eda4f7262f136420c07c331b871bf139f74f3010e3c4fe57df3afb71683', 16)
            this.e = bigInt('10001', 16)
        };

        a2hex(byteArray) {
            var hexString = ''
            var nextHexByte
            for (var i = 0; i < byteArray.length; i++) {
                nextHexByte = byteArray[i].toString(16)
                if (nextHexByte.length < 2) {
                    nextHexByte = '0' + nextHexByte
                }
                hexString += nextHexByte
            }
            return hexString
        }

        hex2a(hex) {
            var str = ''
            for (var i = 0; i < hex.length; i += 2) {
                str += String.fromCharCode(parseInt(hex.substr(i, 2), 16))
            }
            return str
        }

        pkcs1pad2(s, n) {
            if (n < s.length + 11) {
                return null
            }
            var ba = []
            var i = s.length - 1
            while (i >= 0 && n > 0) {
                ba[--n] = s.charCodeAt(i--)
            }
            ba[--n] = 0
            while (n > 2) { // random non-zero pad
                ba[--n] = 0xff
            }
            ba[--n] = 2
            ba[--n] = 0
            var c = this.a2hex(ba)
            return bigInt(c, 16)
        }

        pkcs1unpad2(a) {
            var b = a.toString(16)
            if (b.length % 2 !== 0) {
                b = '0' + b
            }
            var c = this.hex2a(b)
            var i = 1
            while (c.charCodeAt(i) !== 0) {
                i++
            }
            return c.slice(i + 1)
        }

        encrypt(text) {
            var m = this.pkcs1pad2(text, 0x80)
            var c = m.modPow(this.e, this.n)
            var h = c.toString(16)
            while (h.length < 0x80 * 2) {
                h = '0' + h
            }
            return h
        };

        decrypt(text) {
            var ba = []
            var i = 0
            while (i < text.length) {
                ba[i] = text.charCodeAt(i)
                i += 1
            }
            var a = bigInt(this.a2hex(ba), 16)
            var c = a.modPow(this.e, this.n)
            var d = this.pkcs1unpad2(c)
            return d
        };
    }

    const pub_key = '-----BEGIN PUBLIC KEY-----\
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCGhpgMD1okxLnUMCDNLCJwP/P0\
UHVlKQWLHPiPCbhgITZHcZim4mgxSWWb0SLDNZL9ta1HlErR6k02xrFyqtYzjDu2\
rGInUC0BCZOsln0a7wDwyOA43i5NO8LsNory6fEKbx7aT3Ji8TZCDAfDMbhxvxOf\
dPMBDjxP5X3zr7cWgwIDAQAB\
-----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 my_rsa = new MyRsa()
    const priv = window.forge.pki.privateKeyFromPem(private_key)
    const pub = window.forge.pki.publicKeyFromPem(pub_key)
    const g_key_l = [120, 6, 173, 76, 51, 134, 93, 24, 76, 1, 63, 70];
    const g_key_s = [0x29, 0x23, 0x21, 0x5e]
    const g_kts = [240, 229, 105, 174, 191, 220, 191, 138, 26, 69, 232, 190, 125, 166, 115, 184, 222, 143, 231, 196, 69, 218, 134, 196, 155, 100, 139, 20, 106, 180, 241, 170, 56, 1, 53, 158, 38, 105, 44, 134, 0, 107, 79, 165, 54, 52, 98, 166, 42, 150, 104, 24, 242, 74, 253, 189, 107, 151, 143, 77, 143, 137, 19, 183, 108, 142, 147, 237, 14, 13, 72, 62, 215, 47, 136, 216, 254, 254, 126, 134, 80, 149, 79, 209, 235, 131, 38, 52, 219, 102, 123, 156, 126, 157, 122, 129, 50, 234, 182, 51, 222, 58, 169, 89, 52, 102, 59, 170, 186, 129, 96, 72, 185, 213, 129, 156, 248, 108, 132, 119, 255, 84, 120, 38, 95, 190, 232, 30, 54, 159, 52, 128, 92, 69, 44, 155, 118, 213, 27, 143, 204, 195, 184, 245];
    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 += my_rsa.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@0.1.5/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
    }
})()