MSE Dump Tools

Media Source Extensions API 数据 Dump 工具

  1. // ==UserScript==
  2. // @name MSE Dump Tools
  3. // @name:zh-CN MSE Dump Tools
  4. // @name:zh-TW MSE Dump Tools
  5. // @name:ja MSE Dump Tools
  6. // @namespace CloudMoeMediaSourceExtensionsAPIDataDumper
  7. // @version 1.6.6
  8. // @description Media Source Extensions API Data Dump Tool
  9. // @description:zh-CN Media Source Extensions API 数据 Dump 工具
  10. // @description:zh-TW Media Source Extensions API 資料 Dump 工具
  11. // @description:ja Media Source Extensions API データ ダンプ ツール
  12. // @author TGSAN
  13. // @match *://*/*
  14. // @run-at document-start
  15. // @noframes
  16. // @grant GM_unregisterMenuCommand
  17. // @grant GM_registerMenuCommand
  18. // @grant unsafeWindow
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23.  
  24. const DefualtI18N = "zh";
  25. const I18NDict = {
  26. "en": {
  27. "视频": "Video",
  28. "音频": "Audio",
  29. "视频 - 最快播放速度": "Video - Fastest playback speed",
  30. "视频 - 恢复播放速度": "Video - Resume playback speed",
  31. "音频 - 最快播放速度": "Audio - Fastest playback speed",
  32. "音频 - 恢复播放速度": "Audio - Resume playback speed",
  33. "尝试直接下载页面中的视频": "Attempt to directly download the video on the page",
  34. "尝试直接下载页面中的音频": "Attempt to directly download the audio on the page",
  35. "结束 Dump": "End Dump",
  36. "没有找到可以下载的项目。": "No downloadable items found.",
  37. "没有找到可以直接下载的项目,但是找到了": "No directly downloadable items found, but",
  38. "个使用 MSE 的项目,需要在想要下载时点击 “结束 Dump” 按钮来停止存储。": "items using MSE were found. Click the \"End Dump\" button when you want to download to stop storing data.",
  39. "轨道:": "Track:",
  40. "已结束保存。": "has finished saving.",
  41. "下载数据:": "Download data:",
  42. },
  43. "ja": {
  44. "视频": "動画",
  45. "音频": "音声",
  46. "视频 - 最快播放速度": "動画 - 最速再生速度",
  47. "视频 - 恢复播放速度": "動画 - 再生速度を元に戻す",
  48. "音频 - 最快播放速度": "音声 - 最速再生速度",
  49. "音频 - 恢复播放速度": "音声 - 再生速度を元に戻す",
  50. "尝试直接下载页面中的视频": "ページ内の動画を直接ダウンロードを試みる",
  51. "尝试直接下载页面中的音频": "ページ内の音声を直接ダウンロードを試みる",
  52. "结束 Dump": "ダンプを終了",
  53. "没有找到可以下载的项目。": "ダウンロード可能な項目が見つかりませんでした。",
  54. "没有找到可以直接下载的项目,但是找到了": "直接ダウンロード可能な項目は見つかりませんでしたが、",
  55. "个使用 MSE 的项目,需要在想要下载时点击 “结束 Dump” 按钮来停止存储。": "個のMSEを使用している項目が見つかりました。ダウンロードしたい時に「ダンプ終了」ボタンをクリックして、保存を停止する必要があります。",
  56. "轨道:": "トラック:",
  57. "已结束保存。": "保存が完了しました。",
  58. "下载数据:": "ダウンロードデータ:",
  59. }
  60. };
  61.  
  62. function GetI18NString(key) {
  63. let lang = navigator.language || navigator.userLanguage;
  64. lang = lang.substr(0, 2);
  65. if (lang !== DefualtI18N) {
  66. if (I18NDict[lang] === undefined) {
  67. lang = "en";
  68. }
  69. if (I18NDict[lang] && I18NDict[lang][key]) {
  70. return I18NDict[lang][key];
  71. }
  72. }
  73. return key;
  74. }
  75.  
  76. GM_registerMenuCommand(GetI18NString("视频 - 最快播放速度"), function () { document.getElementsByTagName("video")[0].playbackRate = 16 });
  77. GM_registerMenuCommand(GetI18NString("视频 - 恢复播放速度"), function () { document.getElementsByTagName("video")[0].playbackRate = 1 });
  78. GM_registerMenuCommand(GetI18NString("音频 - 最快播放速度"), function () { document.getElementsByTagName("audio")[0].playbackRate = 16 });
  79. GM_registerMenuCommand(GetI18NString("音频 - 恢复播放速度"), function () { document.getElementsByTagName("audio")[0].playbackRate = 1 });
  80. GM_registerMenuCommand(GetI18NString("尝试直接下载页面中的视频"), function () { DirectDownloadPlayingVideo("video") });
  81. GM_registerMenuCommand(GetI18NString("尝试直接下载页面中的音频"), function () { DirectDownloadPlayingVideo("audio") });
  82. GM_registerMenuCommand(GetI18NString("结束 Dump"), EndAllDumpTasks);
  83.  
  84. var dumpEndTasks = [];
  85.  
  86. function dateFormat(dataObj, fmt) {
  87. var o = {
  88. "M+": dataObj.getMonth() + 1, // 月份
  89. "d+": dataObj.getDate(), // 日
  90. "h+": dataObj.getHours(), // 小时
  91. "m+": dataObj.getMinutes(), // 分
  92. "s+": dataObj.getSeconds(), // 秒
  93. "q+": Math.floor((dataObj.getMonth() + 3) / 3), // 季度
  94. "S": dataObj.getMilliseconds() // 毫秒
  95. };
  96. if (/(y+)/.test(fmt)) {
  97. fmt = fmt.replace(RegExp.$1, (dataObj.getFullYear() + "").substr(4 - RegExp.$1.length));
  98. }
  99. for (var k in o) {
  100. if (new RegExp("(" + k + ")").test(fmt)) {
  101. fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
  102. }
  103. }
  104. return fmt;
  105. }
  106.  
  107. async function DirectDownloadPlayingVideo(tag) {
  108. let elements = document.getElementsByTagName(tag);
  109. let downloadCount = 0;
  110. let mseCount = 0;
  111. for (let i = 0; i < elements.length; i++) {
  112. let videoLink = document.getElementsByTagName("video")[i].currentSrc;
  113. if (videoLink == "") {
  114. continue;
  115. }
  116. if (videoLink.startsWith("blob:")) {
  117. mseCount++;
  118. continue;
  119. }
  120. let a = document.createElement('a');
  121. a.download = "direct_" + tag;
  122. var res = await fetch(videoLink);
  123. var videoBlob = await res.blob();
  124. var url = window.URL.createObjectURL(videoBlob);
  125. a.href = url;
  126. a.click();
  127. a.remove();
  128. window.URL.revokeObjectURL(url);
  129. downloadCount++;
  130. }
  131. if (downloadCount == 0) {
  132. if (mseCount == 0) {
  133. alert(GetI18NString("没有找到可以下载的项目。"));
  134. } else {
  135. alert(GetI18NString("没有找到可以直接下载的项目,但是找到了") + " " + mseCount + " " + GetI18NString("个使用 MSE 的项目,需要在想要下载时点击 “结束 Dump” 按钮来停止存储。"));
  136. }
  137. }
  138. }
  139.  
  140. function EndAllDumpTasks() {
  141. while (dumpEndTasks.length > 0) {
  142. let endTask = dumpEndTasks.shift();
  143. endTask();
  144. }
  145. }
  146.  
  147. unsafeWindow.SavedDataList = [];
  148.  
  149. unsafeWindow.DownloadData = function (dataKey, fileName) {
  150. const link = document.createElement('a');
  151. link.href = URL.createObjectURL(new Blob([unsafeWindow.SavedDataList[dataKey]]));
  152. link.download = fileName;
  153. link.click();
  154. window.URL.revokeObjectURL(link.href);
  155. }
  156.  
  157. function DownloadDataCmd(key, ext, type, date) {
  158. if (ext == "mp4" && type == "audio") {
  159. ext = "m4a";
  160. }
  161. DownloadData(key, "dumped_" + type + "_" + dateFormat(date, "yyyyMMddhhmmss") + "." + ext);
  162. }
  163.  
  164. function Uint8ArrayConcat(a, b) {
  165. var c = new Uint8Array(a.length + b.length);
  166. c.set(a);
  167. c.set(b, a.length);
  168. return c;
  169. }
  170.  
  171. function BytesToSize(bytes) {
  172. if (bytes === 0) return '0 B';
  173. var k = 1024;
  174. var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  175. var i = Math.floor(Math.log(bytes) / Math.log(k));
  176. return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
  177. }
  178.  
  179. var _addSourceBuffer = unsafeWindow.MediaSource.prototype.addSourceBuffer;
  180. unsafeWindow.MediaSource.prototype.addSourceBuffer = function (mime) {
  181. console.log("MediaSource addSourceBuffer Type: ", mime);
  182. mime = mime.trim();
  183. const regex = /^.+\/(.+);\s*codecs=\"{0,1}(.+?)\"{0,1}$/g;
  184. let mimeMatches = regex.exec(mime);
  185. let format = "bin";
  186. let codecs = "";
  187. let basicCodecs = "";
  188. if (mimeMatches != null && mimeMatches.length == 3) {
  189. format = mimeMatches[1];
  190. codecs = mimeMatches[2];
  191. codecs = codecs.replace("\"", "");
  192. let basicCodecsArray = codecs.split(",");
  193. for (let i = 0; i < basicCodecsArray.length; i++) {
  194. let basicCodec = basicCodecsArray[i];
  195. let indexOfBasicCodec = basicCodec.indexOf(".");
  196. if (indexOfBasicCodec > 0) {
  197. basicCodec = basicCodec.substring(0, indexOfBasicCodec);
  198. }
  199. if (i == 0) {
  200. basicCodecs = basicCodec;
  201. } else {
  202. basicCodecs = basicCodecs + "," + basicCodec;
  203. }
  204. }
  205. }
  206. var sourceBuffer = _addSourceBuffer.call(this, mime);
  207. var _append = sourceBuffer.appendBuffer;
  208. var endToSave = false;
  209. var sourceBufferData = new Uint8Array();
  210. var isVideo = (mime.startsWith("audio") ? false : true);
  211. var type = (isVideo ? "video" : "audio");
  212. var key = type + "_" + window.performance.now().toString();
  213. var startDate = new Date();
  214. dumpEndTasks.push(() => {
  215. endToSave = true;
  216. console.warn(GetI18NString("轨道:") + " " + mime + " " + GetI18NString("已结束保存。"));
  217. unsafeWindow.SavedDataList[key] = sourceBufferData;
  218. let downloadCaption = `${GetI18NString("下载数据:")} ${(isVideo ? GetI18NString("视频") : GetI18NString("音频"))} ${basicCodecs != "" ? (basicCodecs) : ""} (${BytesToSize(sourceBufferData.length)}, at ${dateFormat(startDate, "hh:mm:ss")})`;
  219. GM_registerMenuCommand(downloadCaption, () => { DownloadDataCmd(key, format, type, startDate); });
  220. });
  221. sourceBuffer.appendBuffer = function (buffer) {
  222. if (!endToSave) {
  223. sourceBufferData = Uint8ArrayConcat(sourceBufferData, new Uint8Array(buffer));
  224. }
  225. _append.call(this, buffer);
  226. }
  227. return sourceBuffer;
  228. }
  229.  
  230. })();