微博一键下载(9宫格&&视频)

一个兴趣使然的脚本,微博一键下载脚本。傻瓜式🐵(简单🍎、易用🧩、可靠💪)

// ==UserScript==
// @name         微博一键下载(9宫格&&视频)
// @namespace    https://github.com/wah0713/getWeiboResources
// @version      2.3.9
// @description  一个兴趣使然的脚本,微博一键下载脚本。傻瓜式🐵(简单🍎、易用🧩、可靠💪)
// @supportURL   https://github.com/wah0713/getWeiboResources/issues
// @author       wah0713
// @compatible   chrome
// @license      MIT
// @icon         https://weibo.com/favicon.ico
// @require      https://code.jquery.com/jquery-1.12.4.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/m3u8-parser/6.0.0/m3u8-parser.min.js
// @match        *://weibo.com/*
// @match        *://*.weibo.com/*
// @match        *://t.cn/*
// @connect      sinaimg.cn
// @connect      weibo.com
// @connect      weibocdn.com
// @connect      miaopai.com
// @connect      qq.com
// @connect      youku.com
// @connect      weibo.com
// @connect      cibntv.net
// @connect      data.video.iqiyi.com
// @connect      cache.m.iqiyi.com
// @connect      *
// @noframes     true
// @run-at       document-idle
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(async function () {
    const $frameContent = $('.Frame_content_3XrxZ')
    const $mMain = $('.m-main')
    let $main = ''
    let $cardList = ''
    let cardHeadStr = ''
    let cardHeadAStr = ''
    if ($frameContent.length === 0 && $mMain.length) {
        // 搜索页面
        $main = $mMain
        $cardList = $('.main-full')
        cardHeadStr = 'div.card-feed  div.from'
        cardHeadAStr = 'a[suda-data]'
    } else if ($frameContent.length && $mMain.length === 0) {
        // 默认页面
        $main = $frameContent
        $cardList = $('.Main_full_1dfQX,.Frame_wrap_16as0')
        cardHeadStr = '.head-info_info_2AspQ'
        cardHeadAStr = '.head-info_time_6sFQg'
    } else {
        return false
    }

    // 第一次使用
    let isFirst = GM_getValue('isFirst', true)
    // 是否开启dubug模式
    let isDebug = false

    let timer = null
    // 消息
    const message = {
        init: '', // 初始化
        getReady: '准备中',
        isEmptyError: '失败,未找到资源',
        // todo 说不定以后想做直播资源下载
        isLiveError: '失败,直播资源解析失败',
        isUnkownError: '失败,未知错误(点击重试)',
        finish: '完成'
    }
    // 左边显示的消息数
    let messagesNumber = GM_getValue('messagesNumber', 5)
    const max = 40
    const min = 3

    // 左侧通知
    const notice = {
        completedQuantity: 0,
        messagelist: []
    }

    const nameAll = {
        userName: '用户名',
        userID: '用户ID',
        mblogid: '微博(文章)ID',
        time: '时间',
        geoName: '定位',
        region: 'IP区域',
        text: '微博文本(前20字)',
    }
    let nameArr = GM_getValue('nameArr', ['userName', 'time'])

    const config = {
        isSpecialHandlingName: {
            name: '替换下载名中【特殊符号】为下划线【_】',
            value: GM_getValue('isSpecialHandlingName', false)
        },
        isSaveHistory: {
            name: '左侧消息是否保存',
            value: GM_getValue('isSaveHistory', false)
        },
        isAutoHide: {
            name: '左侧消息自动消失',
            value: GM_getValue('isAutoHide', false)
        },
        isShowActive: {
            name: '左侧消息过滤【已经完成】',
            value: GM_getValue('isShowActive', false)
        },
        isIncludesText: {
            name: '下载文件中包含【微博文本】',
            value: GM_getValue('isIncludesText', false)
        },
        isVideoHD: {
            name: '是否下载最高清的【视频】',
            value: GM_getValue('isVideoHD', false)
        },
        isImageHD: {
            name: '是否下载最高清的【图片】(会明显增加下载耗时)',
            value: GM_getValue('isImageHD', false)
        },
        isPack: {
            name: '是否打包下载(压缩包)',
            value: GM_getValue('isPack', true)
        }
    }

    // 递归proxy
    function reactive(data, callBack) {
        return new Proxy(data, {
            set(target, propKey, value, receiver) {
                callBack && callBack(target, propKey, value, receiver)
                if (typeof value === 'object') {
                    value = reactive(value, callBack)
                }
                return Reflect.set(target, propKey, value, receiver)
            }
        })
    }

    const data = reactive({}, (target, propKey, value, receiver) => {
        const {
            name,
        } = target
        if (propKey === 'message') {
            // 数据变化更新消息
            retextDom($(`${cardHeadStr}:has(>[href="${name}"])`), value)
            handleMessage(target, value)
        }
    })

    // 读取缓存中的data
    const getCacheData = () => {
        const cacheData = JSON.parse(GM_getValue('cacheData', '{}'));
        [...Object.keys(cacheData)].forEach(item => {
            data[item] = cacheData[item]
        })
    }

    if (config.isSaveHistory.value) {
        // 第一次打开页面
        notice.messagelist = JSON.parse(GM_getValue('noticeMessagelist', '[]'))
        getCacheData()

        // 打开不同页签时,加载data
        document.addEventListener('visibilitychange', function () {
            if (!document.hidden) return false
            notice.messagelist = JSON.parse(GM_getValue('noticeMessagelist', '[]'))
            getCacheData()
        });
    }

    const filterData = () => {
        const keyList = Object.keys(data)
        const max = 50
        if (keyList.length > max) {
            // 按[下载时间]排序
            const newKeyList = keyList.sort((a, b) => {
                return data[b].startTime - data[a].startTime
            })
            // 删除data过多的部分
            newKeyList.slice(max).forEach(item => {
                delete data[item]
            })
        }
    }

    const updateCacheData = () => {
        const cacheData = JSON.parse(JSON.stringify(data));
        [...Object.keys(cacheData)].forEach(item => {
            cacheData[item].completedQuantity = null
            // 未下载完成状态初始化
            if (cacheData[item].message !== message.finish) {
                cacheData[item].message = message.init
            }
        })

        // 保存data
        GM_setValue('cacheData', JSON.stringify(cacheData))
    }

    function handleMessage(target, value) {
        const {
            name,
            title,
            percentage
        } = target

        // title为空,即未初始化
        if (title === '') {
            return false
        }

        // 左侧消息是否保存
        if (config.isShowActive.value) {
            notice.messagelist = notice.messagelist.filter(item => item.message !== '下载' + message.finish)
        }

        notice.messagelist = notice.messagelist.filter(item => item.href !== name).slice(-(messagesNumber - 1))
        notice.messagelist.push({
            href: name,
            title,
            percentage,
            message: `下载${value}`
        })

        const list = [...Object.keys(data)]
        notice.completedQuantity = list.length;
        list.forEach(item => {
            let {
                completedQuantity,
                total,
            } = data[item]

            if (completedQuantity === total) {
                notice.completedQuantity--
                GM_setValue('noticeMessagelist', JSON.stringify(notice.messagelist.map(item => {
                    let str = item.message
                    if (item.message !== ('下载' + message.finish)) {
                        str = '下载' + message.init
                    }
                    return {
                        ...item,
                        message: str
                    }
                })))

            } else if (completedQuantity === null) {
                notice.completedQuantity--
            }

        })

        const tempList = JSON.parse(JSON.stringify(notice.messagelist))

        $('#wah0713 .container .showMessage').html(`
            <p><span>进行中的下载任务数:</span><span class="red">${notice.completedQuantity}</span></p>
            ${tempList.reverse().map(item=>{
                return `<p><a href="${item.href}" style="background-image: linear-gradient(to right,var(--w-main) ${item.percentage}%,#91c6ca 0);" target="_blank" title="打开微博详情">${item.title}</a><span>:</span><span data-href=${item.href} class="red downloadBtn" title="点击再次下载">${item.message}</span></p>`
            }).join('')}
        `)

        clearTimeout(timer)
        $('#wah0713').removeClass('out')
        if (config.isAutoHide.value && notice.completedQuantity === 0) {
            timer = setTimeout(() => {
                $('#wah0713').addClass('out')
            }, 5000)
        }
    }

    // 获取资源链接
    async function getFileUrlByInfo(dom) {
        const id = $(dom).children('a').attr('href').match(/(?<=\d+\/)(\w+)/) && RegExp.$1
        const {
            isLive,
            topMedia,
            pic_infos,
            mix_media_info,
            text_raw,
            isLongText,
            mblogid,
            region_name,
            geo,
            created_at,
            mblog_vip_type,
            user: {
                screen_name,
                idstr
            }
        } = await getInfoById(id)

        const date = new Date(created_at)
        const Y = date.getFullYear()
        const M = formatNumber(date.getMonth() + 1)
        const D = formatNumber(date.getDate())
        const H = formatNumber(date.getHours())
        const m = formatNumber(date.getMinutes())
        const time = `${Y}-${M}-${D} ${H}:${m}`

        const urlData = {};

        // 图片
        if (pic_infos) {
            const arr = [...Object.keys(pic_infos)]
            arr.forEach((ele, index) => {
                const afterName = arr.length === 1 ? '' : `-part${formatNumber(index + 1)}`

                // 超高清图源
                let url = `https://weibo.com/ajax/common/download?pid=${ele}`
                // 高清图源
                const mw2000Url = get(pic_infos[ele], 'mw2000.url', '')

                // 粉丝专属||普通画质图片
                if (mblog_vip_type === 1 || !config.isImageHD.value || getSuffixName(mw2000Url) === 'gif') {
                    url = mw2000Url
                }

                urlData[`${afterName}.${getSuffixName(mw2000Url)}`] = url

                // live视频
                if (pic_infos[ele].type === 'livephoto') {
                    const url = get(pic_infos[ele], 'video', '')
                    urlData[`${afterName}.${getSuffixName(url)}`] = url
                }
            })
        }

        // 图片加视频
        if (mix_media_info) {
            for (let index = 0; index < mix_media_info.items.length; index++) {
                const ele = mix_media_info.items[index];
                const afterName = mix_media_info.items.length === 1 ? '' : `-part${formatNumber(index + 1)}`

                let imgUrl = null
                let mediaUrl = null
                let videoHDUrl = null
                if (ele.type === "video") {
                    objectId = get(ele, 'data.object_id', '')
                    if (config.isVideoHD.value && objectId) {
                        videoHDUrl = await getVideoHD(objectId)
                    }

                    imgUrl = get(ele, 'data.pic_info.pic_big.url', '')
                    mediaUrl = videoHDUrl || get(ele, 'data.media_info.stream_url_hd', get(ele, 'data.media_info.stream_url', ''))
                } else {
                    imgUrl = get(ele, 'data.mw2000.url', '')
                    // live视频
                    if (get(ele, 'data.type', '') === 'livephoto') {
                        const url = get(ele, 'data.video', '')
                        urlData[`${afterName}.${getSuffixName(url)}`] = url
                    }
                }

                if (!config.isImageHD.value || getSuffixName(imgUrl) === 'gif') {
                    // 普通图片
                    urlData[`${afterName}.${getSuffixName(imgUrl)}`] = imgUrl
                } else {
                    // 高清图片
                    urlData[`${afterName}.${getSuffixName(imgUrl)}`] = `https://weibo.com/ajax/common/download?pid=${imgUrl.match(/([\w]+)(?=\.\w+$)/)&& RegExp.$1}`
                }

                if (mediaUrl) {
                    urlData[`${afterName}.${getSuffixName(mediaUrl)}`] = mediaUrl
                }
            }
        }

        // 视频
        if (topMedia) {
            urlData.media = topMedia
        }

        return {
            isLive,
            urlData,
            time,
            geo,
            isLongText,
            mblogid,
            text: text_raw,
            regionName: region_name,
            userName: screen_name,
            userID: idstr,
        }
    }

    // 判断为空图片
    function isEmptyFile(res) {
        const size = get(res, '_blob.size', 0)
        const finalUrl = get(res, 'finalUrl', '')
        if (finalUrl.endsWith('gif#101') ||
            size <= 200 ||
            // gif
            (/\.gif\r\n/.test(res.responseHeaders) && size <= 6000)) {
            return true
        }
        return false
    }

    // 获取后缀
    function getSuffixName(url) {
        let suffixName = new URL(url).pathname.match(/\.(\w+)$/) && RegExp.$1
        if (['json', null].includes(suffixName)) {
            suffixName = 'mp4'
        }
        return suffixName
    }

    // 处理名称
    function getFileName({
        time,
        userName,
        userID,
        regionName,
        geo,
        text,
        mblogid,
    }) {

        const region = regionName && regionName.match(/\s(.*)/) && RegExp.$1 || ''
        const geoName = get(geo, 'detail.title', '')
        text = text.slice(0, 20)

        const nameObj = {
            time,
            userName,
            userID,
            region,
            geoName,
            text,
            mblogid,
        }

        let title = ''
        for (let i = 0; i < nameArr.length; i++) {
            const item = nameArr[i];
            if (nameObj[item]) {
                title += ` ${nameObj[item]}`
            }
        }
        title = title.trim()
        // 替换下载名中【特殊符号】为下划线【_】
        if (config.isSpecialHandlingName.value) {
            title = title.replace(/[\<|\>|\\|\/|;|:|\*|\?|\$|@|\&|\(|\)|\"|\'|#|\|]/g, '_')
        }
        return title
    }

    // 打包
    function pack(resBlob, modification) {
        const zip = new JSZip();
        resBlob.forEach(function (obj) {
            const name = `${modification}${obj._lastName}`
            zip.file(name, obj._blob);
        });
        return new Promise(async (resolve, reject) => {
            // 生成zip文件并下载
            resolve(await zip.generateAsync({
                type: 'blob'
            }))
        })
    }

    // 模拟点击下载
    function download(url, fileName) {
        const a = document.createElement('a')
        a.setAttribute('href', url)
        a.setAttribute('download', fileName)
        a.click()
        a.remove()
        // 释放URL
        URL.revokeObjectURL(url)
    }

    // 下载流(文本)
    async function getTextBlob({
        text,
        href,
        isLongText
    }) {
        let content = text;
        if (isLongText) {
            content = await getLongtextById(href.match(/(?<=\d+\/)(\w+)/) && RegExp.$1) || text
        }

        const _blob = new Blob([content], {
            type: "text/plain;charset=utf-8",
        });

        return {
            _blob,
            _lastName: '.txt',
            finalUrl: 'https://github.com/wah0713/text.txt'
        }
    }

    // 下载流
    function getFileBlob(url, _lastName, options, limt = 3) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                url,
                method: 'get',
                responseType: 'blob',
                headers: {
                    referer: 'https://weibo.com/',
                    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'
                },
                onload: (res) => {
                    isDebug && console.log(`getFileBlob-onload`, res)

                    const returnBlob = {
                        ...res,
                        _blob: res.response,
                        _lastName
                    }
                    options.callback && options.callback(returnBlob)

                    // 下载失败,也会正常返回空文件
                    const {
                        size,
                        type
                    } = res.response;
                    if (size <= 200 && type === "text/html; charset=utf-8") {
                        resolve(null)
                    }

                    resolve(returnBlob)
                },
                onerror: async (res) => {
                    console.error(`getFileBlob-onerror`, res)
                    if (limt > 0) {
                        resolve(await getFileBlob(url, _lastName, options, --limt))
                    } else {
                        resolve(null)
                    }
                },
                onprogress: (res) => {
                    options.onprogress && options.onprogress(res)
                }
            })
        })
    }

    // 通过id获取链接
    function getInfoById(id) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                url: `https://weibo.com/ajax/statuses/show?id=${id}`,
                responseType: 'json',
                onload: (res) => {
                    isDebug && console.log(`getInfoById-onload`, res)
                    const response = res.response
                    response.topMedia = ''

                    try {
                        // retweeted_status 为转发
                        if (res.response.retweeted_status) {
                            response.pic_infos = res.response.retweeted_status.pic_infos
                            response.mix_media_info = res.response.retweeted_status.mix_media_info
                            // 粉丝专属
                            response.mblog_vip_type = res.response.retweeted_status.mblog_vip_type
                        }

                        // 视频
                        if (res.response.page_info) {
                            const {
                                isLive,
                                url
                            } = handleMedia(res)

                            response.topMedia = url
                            // 直播资源
                            response.isLive = isLive
                        }
                    } catch (error) {}
                    resolve(response)
                },
                onerror: (res) => {
                    console.error(`getInfoById-onerror`, res)
                    resolve(null)
                }
            })
        })
    }

    // 获取最高分辨率视频
    function getVideoHD(id) {
        const formData = new FormData();
        formData.append("data", `{"Component_Play_Playinfo":{"oid":"${id}"}}`);
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'post',
                responseType: 'json',
                url: `https://weibo.com/tv/api/component?page=/tv/show/${id}`,
                data: formData,
                headers: {
                    referer: 'https://weibo.com/',
                },
                onload: (res) => {
                    isDebug && console.log(`getVideoHD-onload`, res)
                    const urls = get(res.response, 'data.Component_Play_Playinfo.urls', {})
                    const newUrls = Object.values(urls).map(item => 'https:' + item)
                    const c = newUrls.sort((a, b) => {
                        (new URLSearchParams(a)).get("template").match(/(\d+)x(\d+)/);
                        const A = RegExp.$1 * RegExp.$2;
                        (new URLSearchParams(b)).get("template").match(/(\d+)x(\d+)/);
                        const B = RegExp.$1 * RegExp.$2
                        return B - A
                    })
                    resolve(c[0])
                },
                onerror: (res) => {
                    console.error(`getVideoHD-onerror`, res)
                    resolve(null)
                }
            })
        })
    }

    // 视频资源解析
    function handleMedia(res) {
        const objectType = get(res.response, 'page_info.object_type', '')
        const url = get(res.response, 'page_info.media_info.playback_list[0].play_info.url', get(res.response, 'page_info.media_info.stream_url', ''))
        return {
            isLive: objectType === 'live',
            url
        }
    }

    // 将blob转为text
    function blobToText(blob) {
        return new Promise((resolve, reject) => {
            let reader = new FileReader()
            reader.readAsText(blob, "utf-8")
            reader.addEventListener("loadend", () => {
                // text格式
                resolve(reader.result)
            })
        })
    }

    // 等待
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }

    // 通过id获取长文
    function getLongtextById(id) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                url: `https://weibo.com/ajax/statuses/longtext?id=${id}`,
                responseType: 'json',
                onload: (res) => {
                    isDebug && console.log(`getLongtextById-onload`, res)
                    const response = res.response
                    resolve(response.data.longTextContent)
                },
                onerror: (res) => {
                    console.error(`getLongtextById-onerror`, res)
                    resolve(null)
                }
            })
        })
    }

    // 作者: 沐秋Alron
    // 链接: https://juejin.cn/post/7099344493010223134
    class TaskQueue {
        constructor(options = {
            num: 10,
            sleepTime: 0
        }) {
            this.originMax = options.num || 10; // 原始最大并发数
            this.sleepTime = () => {
                // 等待时间
                if (options.sleepTime) {
                    return options.sleepTime()
                }
                return 0
            }; // 等待时间
            this.max = this.originMax; // 最大并发数
            this.index = 0 // 下标
            this.taskList = [] // 用shift方法实现先进先出
            this.resList = [] // 最后返回队列数组
            this.isError = false // 任务失败
            this.maxLength // 总任务数量
        }

        addTask(task) {
            this.taskList.push({
                task,
                index: this.index++
            });
        }

        async start() {
            return await this.run()
        }

        async end() {
            if (this.isError) return false

            if (this.maxLength === this.resList.filter(Boolean).length) {
                // 任务完成
                return this.resList
            }

            await sleep(200)
            // 自动进行下一个任务
            return await this.run(1)
        }

        run(taskNum = this.max) {
            return new Promise(async (resolve, reject) => {
                const length = this.taskList.length;
                if (!length) {
                    resolve(await this.end())
                    return false;
                }
                // 控制并发数量
                const min = Math.min(length, taskNum);
                for (let i = 0; i < min; i++) {
                    // 开始占用一个任务的空间
                    this.max--;

                    const {
                        task,
                        index
                    } = this.taskList.shift();

                    if (index === 0) {
                        // 保存最大任务数
                        this.maxLength = length
                    } else if (taskNum !== 1) {
                        // 第一个和任务数为1不需要等待
                        await sleep(this.sleepTime())
                    }

                    task().then((res) => {
                        if (res === null) {
                            // 任意一个失败
                            this.isError = true
                            resolve(false)
                            return false
                        }
                        this.resList[index] = res
                    }).finally(async () => {
                        // 任务完成,释放空间
                        this.max++;

                        resolve(await this.end())
                    })
                }
            })
        }
    }

    // 下载视频
    async function DownLoadMedia({
        href,
        urlData,
        text,
        isLongText
    }) {
        const mediaRes = await getFileBlob(urlData.media, `.${getSuffixName(urlData.media)}`, {
            onprogress: (res) => {
                const {
                    loaded,
                    totalSize
                } = res
                const completedQuantity = loaded
                const total = totalSize
                data[href].completedQuantity = completedQuantity
                data[href].total = total
                const percentage = completedQuantity / total * 100

                data[href].percentage = percentage
                data[href].message = `中${formatNumber(completedQuantity / 1024/ 1024)}/${formatNumber(total / 1024/ 1024)}M(${formatNumber(percentage)}%)`
            }
        })
        if (!get(mediaRes, '_blob', null)) {
            return false
        }

        if (!get(mediaRes, '_blob.type', '').startsWith('video')) {
            const parser = new m3u8Parser.Parser();
            parser.push(await blobToText(mediaRes._blob));
            parser.end();

            const urlArr = parser.manifest.segments.map(item => {
                let url
                try {
                    new URL(item.uri)
                    url = item.uri
                } catch (error) {
                    url = `${new URL(urlData.media).origin}/${item.uri}`
                }
                return url
            });

            data[href].completedQuantity = 0
            const total = urlArr.length

            const taskQueue = new TaskQueue();
            urlArr.forEach(item =>
                taskQueue.addTask(getFileBlob.bind(null, item, '', {
                    callback: () => {
                        data[href].completedQuantity++
                        const completedQuantity = data[href].completedQuantity

                        const percentage = new Intl.NumberFormat(undefined, {
                            maximumFractionDigits: 2
                        }).format(completedQuantity / total * 100)

                        data[href].percentage = percentage
                        data[href].message = `中${completedQuantity}/${total}(${percentage}%)`
                    }
                }))
            )

            const taskQueueRes = await taskQueue.start()
            if (taskQueueRes === false) {
                // 解析失败
                return false
            }

            mediaRes._blob = new Blob(taskQueueRes.map(item => item._blob), {
                type: 'video/MP2T'
            })
            mediaRes._lastName = '.mp4'
        }

        if (text) {
            const textBlob = await getTextBlob({
                text,
                href,
                isLongText
            })

            if (config.isPack.value) {
                download(URL.createObjectURL(await pack([mediaRes, textBlob], data[href].title)), `${data[href].title}.zip`)
            } else {
                download(URL.createObjectURL(textBlob._blob), `${data[href].title}${textBlob._lastName}`)
                download(URL.createObjectURL(mediaRes._blob), `${data[href].title}${mediaRes._lastName}`)
            }

        } else {
            download(URL.createObjectURL(mediaRes._blob), `${data[href].title}${mediaRes._lastName}`)
        }
        return true
    }

    // 下载(默认)
    async function DownLoadDefault({
        href,
        urlData,
        urlArr,
        text = '',
        isLongText
    }) {
        const total = urlArr.length
        data[href].total = total

        let sleepTime = null
        // 错开开始时间,减少接口调用失败率
        if (config.isImageHD.value) {
            sleepTime = () => (
                800 + Math.random() * 500
            )

        }

        const taskQueue = new TaskQueue({
            num: 3,
            sleepTime
        });

        urlArr.forEach(item =>
            taskQueue.addTask(getFileBlob.bind(null, urlData[item], item, {
                callback: (returnBlob) => {
                    data[href].completedQuantity++
                    const completedQuantity = data[href].completedQuantity

                    const percentage = new Intl.NumberFormat(undefined, {
                        maximumFractionDigits: 2
                    }).format(completedQuantity / total * 100)

                    data[href].percentage = percentage
                    data[href].message = `中${completedQuantity}/${total}(${percentage}%)`

                    if (!config.isPack.value && !isEmptyFile(returnBlob)) {
                        download(URL.createObjectURL(returnBlob._blob), `${data[href].title}${returnBlob._lastName}`)
                    }
                }
            }))
        )

        let taskQueueRes = await taskQueue.start()
        if (taskQueueRes === false) {
            // 解析失败
            return false
        }

        taskQueueRes = taskQueueRes.filter(item => !isEmptyFile(item));

        if (text) {
            const textBlob = await getTextBlob({
                text,
                href,
                isLongText
            })

            if (!config.isPack.value) {
                download(URL.createObjectURL(textBlob._blob), `${data[href].title}${textBlob._lastName}`)
            }

            taskQueueRes.push(textBlob)
        }

        if (!config.isPack.value) return true

        if (taskQueueRes.length === 0) {
            return null
        } else if (taskQueueRes.length === 1) {
            download(URL.createObjectURL(taskQueueRes[0]._blob), `${data[href].title}${taskQueueRes[0]._lastName}`)
        } else if (taskQueueRes.length > 1) {
            const content = await pack(taskQueueRes, data[href].title)
            download(URL.createObjectURL(content), `${data[href].title}.zip`)
        }
        return true
    }

    // 数字格式化
    function formatNumber(number) {
        return String(new Intl.NumberFormat(undefined, {
            maximumFractionDigits: 2
        }).format(number)).padStart(2, '0')
    }

    // dom修改文本
    function retextDom(dom, text) {
        $(dom).attr('show-text', text)
    }

    /**
     * object: 对象
     * path: 输入的路径
     * defaultVal: 默认值
     * url: https://blog.csdn.net/RedaTao/article/details/108119230
     **/
    function get(object, path, defaultVal = undefined) {
        // 先将path处理成统一格式
        let newPath = [];
        if (Array.isArray(path)) {
            newPath = path;
        } else {
            // 先将字符串中的'['、']'去除替换为'.',split分割成数组形式
            newPath = path.replace(/\[/g, '.').replace(/\]/g, '').split('.');
        }

        // 递归处理,返回最后结果
        return newPath.reduce((o, k) => {
            return (o || {})[k]
        }, object) || defaultVal;
    }

    async function main({
        href,
        urlData,
        text,
        isLongText
    }) {
        filterData()
        updateCacheData()

        if (data[href].isLive) {
            data[href].message = message.isLiveError
            return false
        }

        const urlArr = Object.keys(urlData);
        if (urlArr.length <= 0) {
            // 没有资源
            data[href].message = message.isEmptyError
            return false
        }

        let isSuccess = true

        if (!config.isIncludesText.value) {
            text = ''
        }

        if (urlArr.length === 1 && urlArr[0] === 'media') {
            // 下载视频
            isSuccess = await DownLoadMedia({
                href,
                urlData,
                text,
                isLongText
            })
        } else {
            // 下载(默认)
            isSuccess = await DownLoadDefault({
                href,
                urlData,
                urlArr,
                text,
                isLongText
            })
        }

        if (isSuccess === null) {
            // 没有资源
            data[href].message = message.isEmptyError
        } else if (isSuccess) {
            // 下载成功
            data[href].message = message.finish
        } else {
            // 下载失败
            data[href].message = message.isUnkownError
        }

        updateCacheData()
    }

    // 模拟esc
    function clickEscKey() {
        const evt = document.createEvent('UIEvents');
        Object.defineProperty(evt, 'keyCode', {
            get: function () {
                return this.keyCodeVal;
            }
        });
        Object.defineProperty(evt, 'which', {
            get: function () {
                return this.keyCodeVal;
            }
        });
        evt.keyCodeVal = 27;
        evt.initEvent('keydown', true, true);
        document.body.dispatchEvent(evt);
    }
    // 预览图片时,点击图片关闭预览功能
    $('.imgInstance.Viewer_imgElm_2JHWe').on('click', clickEscKey)

    $main.prepend(`
    <div id="wah0713">
        <div class="container">
            <div class="showMessage"></div>
            <div class="editName">
                <span>可选下载名(【点击】或【拖拽到下方】)</span>
                <ul class="unactive">
                    ${[...Object.keys(nameAll)].filter(item=>!nameArr.includes(item)).map(item=>{
                        return `<li data-id="${item}" draggable="true">${nameAll[item]}</li>`
                    }).join('')}
                </ul>
                <span>当前下载名(【用户名】为必选)</span>
                <ul class="active">
                    ${nameArr.map(item=>{
                        return `<li data-id="${item}" draggable="true">${nameAll[item]}</li>`
                    }).join('')}
                </ul>
            </div>
            <div class="input-box">需要显示的消息条数:<input type="number" max="${max}" min="${min}" value="${messagesNumber}"
                    step=1>
            </div>
        </div>
    </div>
       `)

    let dragstartDom = null;

    function updateNameArr() {
        nameArr = []
        dragstartDom = null;
        [...document.querySelector(`#wah0713 .editName ul.active`).children].forEach(item => {
            nameArr.push(item.dataset.id)
        })
        GM_setValue('nameArr', nameArr)
    }

    [...document.querySelectorAll('#wah0713 .editName ul')].forEach(item => {

        item.addEventListener('dragstart', function (event) {
            if (event.target.nodeName !== 'LI') {
                return false
            }
            dragstartDom = event.target
        });

        item.addEventListener('dragleave', function (event) {
            event.target.classList.remove('outline')
        });

        item.addEventListener('dragover', function (event) {
            if (item.classList.contains('unactive') && dragstartDom.dataset.id === 'userName') {
                event.dataTransfer.dropEffect = 'none';
                return false
            }
            event.preventDefault();
            event.dataTransfer.dropEffect = 'move';
            event.target.classList.add('outline')
        });

        item.addEventListener('drop', function (event) {
            event.target.classList.remove('outline')
            if (event.target.nodeName === 'LI') {
                event.target.insertAdjacentElement("beforeBegin", dragstartDom)
            } else if (event.target.nodeName === 'UL') {
                event.target.insertAdjacentElement("beforeEnd", dragstartDom)
            }
            updateNameArr()
        });

        item.addEventListener('click', function (event) {
            if (event.target.nodeName !== 'LI' || event.target.dataset.id === 'userName') {
                return false
            }
            const className = item.classList.contains('unactive') ? 'active' : 'unactive'
            document.querySelector(`#wah0713 .editName ul.${className}`).insertAdjacentElement("beforeEnd", event.target)
            updateNameArr()
        })
    })

    // 是第一次使用开启
    if (isFirst) {
        $cardList.addClass('isFirst')
    }

    $cardList.on('click', `${cardHeadStr}:not(.Feed_retweetHeadInfo_Tl4Ld)`, async function (event) {
        if (event.target.className !== event.currentTarget.className || ![...Object.values(message).filter(item => item !== message.getReady), undefined].includes(
                $(this).attr('show-text')
            )) return false

        // 关闭第一次使用提示
        if (isFirst) {
            isFirst = false
            GM_setValue('isFirst', false)
            $cardList.removeClass('isFirst')
        }

        const href = $(this).find(cardHeadAStr).attr('href')

        data[href] = {
            name: href,
            urlData: {},
            text: '',
            title: '',
            message: '',
            isLive: false, // 直播资源
            isLongText: false,
            total: 0,
            completedQuantity: 0,
            percentage: 0,
            startTime: Number(new Date()),
        }

        const {
            urlData,
            isLive,
            time,
            userName,
            userID,
            regionName,
            geo,
            text,
            isLongText,
            mblogid,
        } = await getFileUrlByInfo(this)

        data[href].title = getFileName({
            time,
            userName,
            userID,
            regionName,
            geo,
            text,
            mblogid
        })
        data[href].urlData = urlData
        data[href].text = text
        data[href].isLongText = isLongText
        data[href].message = message.getReady
        data[href].isLive = isLive

        main({
            href,
            urlData,
            text,
            isLongText
        })
    })

    $('.showMessage').on('click', '.downloadBtn', async function (event) {
        if (event.target.className !== event.currentTarget.className || ![...Object.values(message).filter(item => item !== message.getReady), undefined].includes($(this).text().replace(/^下载/, ''))) return false
        const href = $(this).data('href')

        data[href].completedQuantity = 0
        data[href].message = message.getReady
        data[href].startTime = Number(new Date())

        main({
            href,
            urlData: data[href].urlData,
            text: data[href].text,
            isLongText: data[href].isLongText,
        })
    })

    $('#wah0713 .container .input-box input').change(event => {
        event.target.value = event.target.value | 0
        if (event.target.value > max) {
            event.target.value = max
        }
        if (event.target.value < min) {
            event.target.value = min
        }
        messagesNumber = event.target.value
        GM_setValue('messagesNumber', messagesNumber)
    })

    const observer = new MutationObserver(() => {
        $(cardHeadStr).attr('show-text', '');
        requestAnimationFrame(() => {
            [...Object.keys(data)].forEach(item => {
                const {
                    message,
                } = data[item]
                retextDom($(`${cardHeadStr}:has(>[href="${item}"])`), message)
            })
        })
    });
    observer.observe($main[0], {
        childList: true,
        subtree: true
    });


    function updateMenuCommand() {
        [...Object.keys(config)].forEach(item => {
            const {
                id,
                value,
                name
            } = config[item]
            if (id) {
                GM_unregisterMenuCommand(id)
            }
            config[item].id = GM_registerMenuCommand(`${value?'✔️':'❌'}${name}`, () => {
                GM_setValue(item, !value)
                config[item].value = !value
                updateMenuCommand()
            })
        })
    }
    updateMenuCommand()

    GM_addStyle(`
body{--red:#ff3852}.head-info_info_2AspQ:not(.Feed_retweetHeadInfo_Tl4Ld)::after,div.card-feed div.from::after{content:"下载" attr(show-text);color:var(--w-brand);cursor:pointer;float:right}.woo-modal-main .wbpro-layer .head-info_info_2AspQ:not(.Feed_retweetHeadInfo_Tl4Ld)::after{content:''}.Main_full_1dfQX.isFirst .head-info_info_2AspQ:not(.Feed_retweetHeadInfo_Tl4Ld)::after,.main-full.isFirst div.card-feed div.from::after{animation:wobble infinite 1s alternate}@keyframes wobble{from{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}to{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.Frame_content_3XrxZ #wah0713,.m-main #wah0713{font-size:12px;font-weight:700}.Frame_content_3XrxZ #wah0713.out,.m-main #wah0713.out{opacity:0}.Frame_content_3XrxZ #wah0713.out:hover,.m-main #wah0713.out:hover{opacity:1}.Frame_content_3XrxZ #wah0713 .container,.m-main #wah0713 .container{background-color:var(--frame-background);position:fixed;left:0;z-index:1}.Frame_content_3XrxZ #wah0713:hover .editName,.Frame_content_3XrxZ #wah0713:hover .input-box,.m-main #wah0713:hover .editName,.m-main #wah0713:hover .input-box{display:block}.Frame_content_3XrxZ #wah0713 input,.m-main #wah0713 input{width:3em;color:var(--w-brand);border-width:1px;outline:0;background-color:transparent}.Frame_content_3XrxZ #wah0713 .input-box,.m-main #wah0713 .input-box{display:none}.Frame_content_3XrxZ #wah0713 .showMessage>p,.m-main #wah0713 .showMessage>p{line-height:16px;margin:4px}.Frame_content_3XrxZ #wah0713 .showMessage>p span,.m-main #wah0713 .showMessage>p span{color:var(--w-main);vertical-align:top}.Frame_content_3XrxZ #wah0713 .showMessage>p span.red,.m-main #wah0713 .showMessage>p span.red{color:var(--w-brand)}.Frame_content_3XrxZ #wah0713 .showMessage>p span.red.downloadBtn,.m-main #wah0713 .showMessage>p span.red.downloadBtn{cursor:pointer}.Frame_content_3XrxZ #wah0713 .showMessage>p a,.m-main #wah0713 .showMessage>p a{color:transparent;overflow:hidden;text-overflow:ellipsis;max-width:300px;display:inline-block;white-space:nowrap;-webkit-background-clip:text}.Frame_content_3XrxZ #wah0713 .showMessage>p a:hover,.m-main #wah0713 .showMessage>p a:hover{text-decoration:none}.Frame_content_3XrxZ #wah0713 .editName,.m-main #wah0713 .editName{display:none;border:1px solid #ccc;padding:2px;border-radius:6px;user-select:none}.Frame_content_3XrxZ #wah0713 .editName ul,.m-main #wah0713 .editName ul{list-style:none;display:flex;height:20px;margin:0;padding:0 10px 0 0;background-color:#fafafa}.Frame_content_3XrxZ #wah0713 .editName li,.m-main #wah0713 .editName li{height:20px;line-height:20px;background:var(--red);color:#fff;padding-inline:3px;margin-left:2px;font-size:12px;cursor:grab;border-radius:5px}.Frame_content_3XrxZ #wah0713 .unactive li,.m-main #wah0713 .unactive li{background:var(--w-brand)}.Frame_content_3XrxZ #wah0713 .outline,.m-main #wah0713 .outline{outline:2px solid #119da6}
`)

    // // debugJS
    // isDebug = true
    // unsafeWindow.$ = $
})()