MSE Dump Tools

Media Source Extensions API Data Dump Tool

// ==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.6
// @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
// @match              *://*/*
// @run-at             document-start
// @noframes
// @grant              GM_unregisterMenuCommand
// @grant              GM_registerMenuCommand
// @grant              unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    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;
    }

})();