MSE Dump Tools

Media Source Extensions API 数据 Dump 工具

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               MSE Dump Tools
// @name:zh-CN         MSE Dump Tools
// @name:zh-TW         MSE Dump Tools
// @name:ja            MSE Dump Tools
// @namespace          CloudMoeMediaSourceExtensionsAPIDataDumper
// @version            1.6.8
// @description        Media Source Extensions API Data Dump Tool
// @description:zh-CN  Media Source Extensions API 数据 Dump 工具
// @description:zh-TW  Media Source Extensions API 資料 Dump 工具
// @description:ja     Media Source Extensions API データ ダンプ ツール
// @author             TGSAN
// @include            /.*/
// @run-at             document-start
// @noframes
// @grant              GM_unregisterMenuCommand
// @grant              GM_registerMenuCommand
// @grant              unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    Object.defineProperty(unsafeWindow.MediaSource, 'canConstructInDedicatedWorker', {
        value: false
    });

    const DefualtI18N = "zh";
    const I18NDict = {
        "en": {
            "视频": "Video",
            "音频": "Audio",
            "视频 - 最快播放速度": "Video - Fastest playback speed",
            "视频 - 恢复播放速度": "Video - Resume playback speed",
            "音频 - 最快播放速度": "Audio - Fastest playback speed",
            "音频 - 恢复播放速度": "Audio - Resume playback speed",
            "尝试直接下载页面中的视频": "Attempt to directly download the video on the page",
            "尝试直接下载页面中的音频": "Attempt to directly download the audio on the page",
            "结束 Dump": "End Dump",
            "没有找到可以下载的项目。": "No downloadable items found.",
            "没有找到可以直接下载的项目,但是找到了": "No directly downloadable items found, but",
            "个使用 MSE 的项目,需要在想要下载时点击 “结束 Dump” 按钮来停止存储。": "items using MSE were found. Click the \"End Dump\" button when you want to download to stop storing data.",
            "轨道:": "Track:",
            "已结束保存。": "has finished saving.",
            "下载数据:": "Download data:",
        },
        "ja": {
            "视频": "動画",
            "音频": "音声",
            "视频 - 最快播放速度": "動画 - 最速再生速度",
            "视频 - 恢复播放速度": "動画 - 再生速度を元に戻す",
            "音频 - 最快播放速度": "音声 - 最速再生速度",
            "音频 - 恢复播放速度": "音声 - 再生速度を元に戻す",
            "尝试直接下载页面中的视频": "ページ内の動画を直接ダウンロードを試みる",
            "尝试直接下载页面中的音频": "ページ内の音声を直接ダウンロードを試みる",
            "结束 Dump": "ダンプを終了",
            "没有找到可以下载的项目。": "ダウンロード可能な項目が見つかりませんでした。",
            "没有找到可以直接下载的项目,但是找到了": "直接ダウンロード可能な項目は見つかりませんでしたが、",
            "个使用 MSE 的项目,需要在想要下载时点击 “结束 Dump” 按钮来停止存储。": "個のMSEを使用している項目が見つかりました。ダウンロードしたい時に「ダンプ終了」ボタンをクリックして、保存を停止する必要があります。",
            "轨道:": "トラック:",
            "已结束保存。": "保存が完了しました。",
            "下载数据:": "ダウンロードデータ:",
        }
    };

    function GetI18NString(key) {
        let lang = navigator.language || navigator.userLanguage;
        lang = lang.substr(0, 2);
        if (lang !== DefualtI18N) {
            if (I18NDict[lang] === undefined) {
                lang = "en";
            }
            if (I18NDict[lang] && I18NDict[lang][key]) {
                return I18NDict[lang][key];
            }
        }
        return key;
    }

    GM_registerMenuCommand(GetI18NString("视频 - 最快播放速度"), function () { document.getElementsByTagName("video")[0].playbackRate = 16 });
    GM_registerMenuCommand(GetI18NString("视频 - 恢复播放速度"), function () { document.getElementsByTagName("video")[0].playbackRate = 1 });
    GM_registerMenuCommand(GetI18NString("音频 - 最快播放速度"), function () { document.getElementsByTagName("audio")[0].playbackRate = 16 });
    GM_registerMenuCommand(GetI18NString("音频 - 恢复播放速度"), function () { document.getElementsByTagName("audio")[0].playbackRate = 1 });
    GM_registerMenuCommand(GetI18NString("尝试直接下载页面中的视频"), function () { DirectDownloadPlayingVideo("video") });
    GM_registerMenuCommand(GetI18NString("尝试直接下载页面中的音频"), function () { DirectDownloadPlayingVideo("audio") });
    GM_registerMenuCommand(GetI18NString("结束 Dump"), EndAllDumpTasks);

    var dumpEndTasks = [];

    function dateFormat(dataObj, fmt) {
        var o = {
            "M+": dataObj.getMonth() + 1,                   // 月份
            "d+": dataObj.getDate(),                        // 日
            "h+": dataObj.getHours(),                       // 小时
            "m+": dataObj.getMinutes(),                     // 分
            "s+": dataObj.getSeconds(),                     // 秒
            "q+": Math.floor((dataObj.getMonth() + 3) / 3), // 季度
            "S": dataObj.getMilliseconds()                  // 毫秒
        };
        if (/(y+)/.test(fmt)) {
            fmt = fmt.replace(RegExp.$1, (dataObj.getFullYear() + "").substr(4 - RegExp.$1.length));
        }
        for (var k in o) {
            if (new RegExp("(" + k + ")").test(fmt)) {
                fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
            }
        }
        return fmt;
    }

    async function DirectDownloadPlayingVideo(tag) {
        let elements = document.getElementsByTagName(tag);
        let downloadCount = 0;
        let mseCount = 0;
        for (let i = 0; i < elements.length; i++) {
            let videoLink = document.getElementsByTagName("video")[i].currentSrc;
            if (videoLink == "") {
                continue;
            }
            if (videoLink.startsWith("blob:")) {
                mseCount++;
                continue;
            }
            let a = document.createElement('a');
            a.download = "direct_" + tag;
            var res = await fetch(videoLink);
            var videoBlob = await res.blob();
            var url = window.URL.createObjectURL(videoBlob);
            a.href = url;
            a.click();
            a.remove();
            window.URL.revokeObjectURL(url);
            downloadCount++;
        }
        if (downloadCount == 0) {
            if (mseCount == 0) {
                alert(GetI18NString("没有找到可以下载的项目。"));
            } else {
                alert(GetI18NString("没有找到可以直接下载的项目,但是找到了") + " " + mseCount + " " + GetI18NString("个使用 MSE 的项目,需要在想要下载时点击 “结束 Dump” 按钮来停止存储。"));
            }
        }
    }

    function EndAllDumpTasks() {
        while (dumpEndTasks.length > 0) {
            let endTask = dumpEndTasks.shift();
            endTask();
        }
    }

    unsafeWindow.SavedDataList = [];

    unsafeWindow.DownloadData = function (dataKey, fileName) {
        const link = document.createElement('a');
        link.href = URL.createObjectURL(new Blob([unsafeWindow.SavedDataList[dataKey]]));
        link.download = fileName;
        link.click();
        window.URL.revokeObjectURL(link.href);
    }

    function DownloadDataCmd(key, ext, type, date) {
        if (ext == "mp4" && type == "audio") {
            ext = "m4a";
        }
        DownloadData(key, "dumped_" + type + "_" + dateFormat(date, "yyyyMMddhhmmss") + "." + ext);
    }

    function Uint8ArrayConcat(a, b) {
        var c = new Uint8Array(a.length + b.length);
        c.set(a);
        c.set(b, a.length);
        return c;
    }

    function BytesToSize(bytes) {
        if (bytes === 0) return '0 B';
        var k = 1024;
        var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
        var i = Math.floor(Math.log(bytes) / Math.log(k));
        return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
    }

    var _addSourceBuffer = unsafeWindow.MediaSource.prototype.addSourceBuffer;
    unsafeWindow.MediaSource.prototype.addSourceBuffer = function (mime) {
        console.log("MediaSource addSourceBuffer Type: ", mime);
        mime = mime.trim();
        const regex = /^.+\/(.+);\s*codecs=\"{0,1}(.+?)\"{0,1}$/g;
        let mimeMatches = regex.exec(mime);
        let format = "bin";
        let codecs = "";
        let basicCodecs = "";
        if (mimeMatches != null && mimeMatches.length == 3) {
            format = mimeMatches[1];
            codecs = mimeMatches[2];
            codecs = codecs.replace("\"", "");
            let basicCodecsArray = codecs.split(",");
            for (let i = 0; i < basicCodecsArray.length; i++) {
                let basicCodec = basicCodecsArray[i];
                let indexOfBasicCodec = basicCodec.indexOf(".");
                if (indexOfBasicCodec > 0) {
                    basicCodec = basicCodec.substring(0, indexOfBasicCodec);
                }
                if (i == 0) {
                    basicCodecs = basicCodec;
                } else {
                    basicCodecs = basicCodecs + "," + basicCodec;
                }
            }
        }
        var sourceBuffer = _addSourceBuffer.call(this, mime);
        var _append = sourceBuffer.appendBuffer;
        var endToSave = false;
        var sourceBufferData = new Uint8Array();
        var isVideo = (mime.startsWith("audio") ? false : true);
        var type = (isVideo ? "video" : "audio");
        var key = type + "_" + window.performance.now().toString();
        var startDate = new Date();
        dumpEndTasks.push(() => {
            endToSave = true;
            console.warn(GetI18NString("轨道:") + " " + mime + " " + GetI18NString("已结束保存。"));
            unsafeWindow.SavedDataList[key] = sourceBufferData;
            let downloadCaption = `${GetI18NString("下载数据:")} ${(isVideo ? GetI18NString("视频") : GetI18NString("音频"))} ${basicCodecs != "" ? (basicCodecs) : ""} (${BytesToSize(sourceBufferData.length)}, at ${dateFormat(startDate, "hh:mm:ss")})`;
            GM_registerMenuCommand(downloadCaption, () => { DownloadDataCmd(key, format, type, startDate); });
        });
        sourceBuffer.appendBuffer = function (buffer) {
            if (!endToSave) {
                sourceBufferData = Uint8ArrayConcat(sourceBufferData, new Uint8Array(buffer));
            }
            _append.call(this, buffer);
        }
        return sourceBuffer;
    }

})();