Greasy Fork镜像 支持简体中文。

m3u8視頻偵測下載器【自動嗅探】

自動檢測頁面m3u8視頻並進行完整下載。檢測到m3u8鏈接後會自動出現在頁面右上角位置,點擊下載即可跳轉到m3u8下載器。

目前為 2022-08-18 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name m3u8视频侦测下载器【自动嗅探】
  3. // @name:zh-CN m3u8视频侦测下载器【自动嗅探】
  4. // @name:zh-TW m3u8視頻偵測下載器【自動嗅探】
  5. // @name:en m3u8 video detector and downloader
  6. // @version 1.1.0
  7. // @description 自动检测页面m3u8视频并进行完整下载。检测到m3u8链接后会自动出现在页面右上角位置,点击下载即可跳转到m3u8下载器。
  8. // @description:zh-CN 自动检测页面m3u8视频并进行完整下载。检测到m3u8链接后会自动出现在页面右上角位置,点击下载即可跳转到m3u8下载器。
  9. // @description:zh-TW 自動檢測頁面m3u8視頻並進行完整下載。檢測到m3u8鏈接後會自動出現在頁面右上角位置,點擊下載即可跳轉到m3u8下載器。
  10. // @description:en Automatically detect the m3u8 video of the page and download it completely. After detecting the m3u8 link, it will automatically appear in the upper right corner of the page. Click download to jump to the m3u8 downloader.
  11. // @icon https://tools.thatwind.com/favicon.png
  12. // @author allFull
  13. // @namespace https://tools.thatwind.com/
  14. // @homepage https://tools.thatwind.com/tool/m3u8downloader
  15. // @match *://*/*
  16. // @require https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js
  17. // @connect *
  18. // @grant unsafeWindow
  19. // @grant GM_openInTab
  20. // @grant GM_getValue
  21. // @grant GM_setValue
  22. // @grant GM_addStyle
  23. // @grant GM_xmlhttpRequest
  24. // @run-at document-start
  25. // ==/UserScript==
  26.  
  27. (function () {
  28. 'use strict';
  29.  
  30. if (location.host === "tools.thatwind.com" || location.host === "localhost:3000") {
  31. GM_addStyle("#userscript-tip{display:none !important;}");
  32.  
  33. // 对请求做代理
  34. const _fetch = unsafeWindow.fetch;
  35. unsafeWindow.fetch = async function (...args) {
  36. try {
  37. let response = await _fetch(...args);
  38. if (response.status !== 200) throw new Error(response.status);
  39. return response;
  40. } catch (e) {
  41. // 失败请求使用代理
  42. if (args.length == 1) {
  43. console.log(`请求代理:${args[0]}`);
  44. return await new Promise((resolve, reject) => {
  45. let referer = new URLSearchParams(location.hash.slice(1)).get("referer");
  46. let headers = {};
  47. if (referer) {
  48. referer = new URL(referer);
  49. headers = {
  50. "origin": referer.origin,
  51. "referer": referer.href
  52. };
  53. }
  54. GM_xmlhttpRequest({
  55. method: "GET",
  56. url: args[0],
  57. responseType: 'arraybuffer',
  58. headers,
  59. onload(r) {
  60. resolve({
  61. status: r.status,
  62. headers: new Headers(r.responseHeaders.split("\n").filter(n => n).map(s => s.split(/:\s*/)).reduce((all, [a, b]) => { all[a] = b; return all; }, {})),
  63. async text() {
  64. return r.responseText;
  65. },
  66. async arrayBuffer() {
  67. return r.response;
  68. }
  69. });
  70. },
  71. onerror() {
  72. reject(new Error());
  73. }
  74. });
  75. });
  76. } else {
  77. throw e;
  78. }
  79. }
  80. }
  81.  
  82. return;
  83. }
  84.  
  85. {
  86. // 请求检测
  87. // const _fetch = unsafeWindow.fetch;
  88. // unsafeWindow.fetch = function (...args) {
  89. // if (checkUrl(args[0])) showM3U({ url: args[0] });
  90. // return _fetch(...args);
  91. // }
  92.  
  93. const _r_text = unsafeWindow.Response.prototype.text;
  94. unsafeWindow.Response.prototype.text = function () {
  95. return new Promise((resolve, reject) => {
  96. _r_text.call(this).then((text) => {
  97. resolve(text);
  98. if (checkContent(text)) showM3U({ url: this.url, content: text });
  99. }).catch(reject);
  100. });
  101. }
  102.  
  103. const _open = unsafeWindow.XMLHttpRequest.prototype.open;
  104. unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
  105. this.addEventListener("load", () => {
  106. try {
  107. let content = this.responseText;
  108. if (checkContent(content)) showM3U({ url: args[1], content });
  109. } catch { }
  110. });
  111. // checkUrl(args[1]);
  112. return _open.apply(this, args);
  113. }
  114.  
  115.  
  116. function checkUrl(url) {
  117. url = new URL(url, location.href);
  118. if (url.pathname.endsWith(".m3u8") || url.pathname.endsWith(".m3u")) {
  119. // 发现
  120. return true;
  121. }
  122. }
  123.  
  124. function checkContent(content) {
  125. if (content.trim().startsWith("#EXTM3U")) {
  126. return true;
  127. }
  128. }
  129.  
  130.  
  131. }
  132.  
  133. const rootDiv = document.createElement("div");
  134. rootDiv.style = `
  135. position: fixed;
  136. z-index: 9999999999999999;
  137. opacity: 0.9;
  138. `;
  139. rootDiv.style.display = "none";
  140. document.documentElement.appendChild(rootDiv);
  141.  
  142. const shadowDOM = rootDiv.attachShadow({ mode: 'open' });
  143. const wrapper = document.createElement("div");
  144. shadowDOM.appendChild(wrapper);
  145.  
  146.  
  147. // 指示器
  148. const bar = document.createElement("div");
  149. bar.style = `
  150. text-align: right;
  151. `;
  152. bar.innerHTML = `
  153. <span
  154. class="number-indicator"
  155. data-number="0"
  156. style="
  157. display: inline-flex;
  158. width: 25px;
  159. height: 25px;
  160. background: black;
  161. padding: 10px;
  162. border-radius: 100px;
  163. margin-bottom: 5px;
  164. cursor: pointer;
  165. "
  166. >
  167. <svg
  168. style="
  169. filter: invert(1);
  170. "
  171. version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 585.913 585.913" style="enable-background:new 0 0 585.913 585.913;" xml:space="preserve">
  172. <g>
  173. <path d="M11.173,46.2v492.311l346.22,47.402V535.33c0.776,0.058,1.542,0.109,2.329,0.109h177.39
  174. c20.75,0,37.627-16.883,37.627-37.627V86.597c0-20.743-16.877-37.628-37.627-37.628h-177.39c-0.781,0-1.553,0.077-2.329,0.124V0
  175. L11.173,46.2z M110.382,345.888l-1.37-38.273c-0.416-11.998-0.822-26.514-0.822-41.023l-0.415,0.01
  176. c-2.867,12.767-6.678,26.956-10.187,38.567l-10.961,38.211l-15.567-0.582l-9.239-37.598c-2.801-11.269-5.709-24.905-7.725-37.361
  177. l-0.25,0.005c-0.503,12.914-0.879,27.657-1.503,39.552L50.84,343.6l-17.385-0.672l5.252-94.208l25.415-0.996l8.499,32.064
  178. c2.724,11.224,5.467,23.364,7.428,34.819h0.389c2.503-11.291,5.535-24.221,8.454-35.168l9.643-33.042l27.436-1.071l5.237,101.377
  179. L110.382,345.888z M172.479,349.999c-12.569-0.504-23.013-4.272-28.539-8.142l4.504-17.249c3.939,2.226,13.1,6.445,22.373,6.687
  180. c12.009,0.32,18.174-5.497,18.174-13.218c0-10.068-9.838-14.683-19.979-14.74l-9.253-0.052v-16.777l8.801-0.066
  181. c7.708-0.208,17.646-3.262,17.646-11.905c0-6.121-4.914-10.562-14.635-10.331c-7.95,0.189-16.245,3.914-20.213,6.446l-4.52-16.693
  182. c5.693-4.008,17.224-8.11,29.883-8.588c21.457-0.795,33.643,10.407,33.643,24.625c0,11.029-6.197,19.691-18.738,24.161v0.314
  183. c12.229,2.216,22.266,11.663,22.266,25.281C213.89,338.188,197.866,351.001,172.479,349.999z M331.104,302.986
  184. c0,36.126-19.55,52.541-51.193,51.286c-29.318-1.166-46.019-17.103-46.019-52.044v-61.104l25.711-1.006v64.201
  185. c0,19.191,7.562,29.146,21.179,29.502c14.234,0.368,22.189-8.976,22.189-29.26v-66.125l28.122-1.097v65.647H331.104z
  186. M359.723,70.476h177.39c8.893,0,16.125,7.236,16.125,16.126v411.22c0,8.888-7.232,16.127-16.125,16.127h-177.39
  187. c-0.792,0-1.563-0.116-2.329-0.232V380.782c17.685,14.961,40.504,24.032,65.434,24.032c56.037,0,101.607-45.576,101.607-101.599
  188. c0-56.029-45.581-101.603-101.607-101.603c-24.93,0-47.749,9.069-65.434,24.035V70.728
  189. C358.159,70.599,358.926,70.476,359.723,70.476z M390.873,364.519V245.241c0-1.07,0.615-2.071,1.586-2.521
  190. c0.981-0.483,2.13-0.365,2.981,0.307l93.393,59.623c0.666,0.556,1.065,1.376,1.065,2.215c0,0.841-0.399,1.67-1.065,2.215
  191. l-93.397,59.628c-0.509,0.4-1.114,0.61-1.743,0.61l-1.233-0.289C391.488,366.588,390.873,365.585,390.873,364.519z" />
  192. </g>
  193. </svg>
  194. </span>
  195. `;
  196.  
  197. wrapper.appendChild(bar);
  198.  
  199. // 样式
  200. const style = document.createElement("style");
  201.  
  202. style.innerHTML = `
  203. .number-indicator{
  204. position:relative;
  205. }
  206.  
  207. .number-indicator::after{
  208. content: attr(data-number);
  209. position: absolute;
  210. bottom: 0;
  211. right: 0;
  212. color: #40a9ff;
  213. font-size: 14px;
  214. font-weight: bold;
  215. background: #000;
  216. border-radius: 10px;
  217. padding: 3px 5px;
  218. }
  219.  
  220. .download-btn:hover{
  221. text-decoration: underline;
  222. }
  223. .download-btn:active{
  224. opacity: 0.9;
  225. }
  226.  
  227. .m3u8-item{
  228. color: white;
  229. margin-bottom: 5px;
  230. display: flex;
  231. flex-direction: row;
  232. background: black;
  233. padding: 3px 10px;
  234. border-radius: 3px;
  235. font-size: 14px;
  236. user-select: none;
  237. }
  238.  
  239. [data-shown="false"] {
  240. opacity: 0.8;
  241. zoom: 0.8;
  242. }
  243.  
  244. [data-shown="false"]:hover{
  245. opacity: 1;
  246. }
  247.  
  248. [data-shown="false"] .m3u8-item{
  249. display: none;
  250. }
  251.  
  252. `;
  253.  
  254. wrapper.appendChild(style);
  255.  
  256.  
  257.  
  258.  
  259. const barBtn = bar.querySelector(".number-indicator");
  260.  
  261. // 关于显隐和移动
  262.  
  263. let shown = GM_getValue("shown", true);
  264. wrapper.setAttribute("data-shown", shown);
  265.  
  266.  
  267. let x = GM_getValue("x", 10);
  268. let y = GM_getValue("y", 10);
  269.  
  270. x = Math.min(innerWidth - 50, x);
  271. y = Math.min(innerHeight - 50, y);
  272.  
  273. if (x < 0) x = 0;
  274. if (y < 0) y = 0;
  275.  
  276. rootDiv.style.top = `${y}px`;
  277. rootDiv.style.right = `${x}px`;
  278.  
  279. barBtn.addEventListener("mousedown", e => {
  280. let startX = e.pageX;
  281. let startY = e.pageY;
  282.  
  283. let moved = false;
  284.  
  285. let mousemove = e => {
  286. let offsetX = e.pageX - startX;
  287. let offsetY = e.pageY - startY;
  288. if (moved || (Math.abs(offsetX) + Math.abs(offsetY)) > 5) {
  289. moved = true;
  290. rootDiv.style.top = `${y + offsetY}px`;
  291. rootDiv.style.right = `${x - offsetX}px`;
  292. }
  293. };
  294. let mouseup = e => {
  295.  
  296. let offsetX = e.pageX - startX;
  297. let offsetY = e.pageY - startY;
  298.  
  299. if (moved) {
  300. x -= offsetX;
  301. y += offsetY;
  302. GM_setValue("x", x);
  303. GM_setValue("y", y);
  304. } else {
  305. shown = !shown;
  306. GM_setValue("shown", shown);
  307. wrapper.setAttribute("data-shown", shown);
  308. }
  309.  
  310. removeEventListener("mousemove", mousemove);
  311. removeEventListener("mouseup", mouseup);
  312. }
  313. addEventListener("mousemove", mousemove);
  314. addEventListener("mouseup", mouseup);
  315. });
  316.  
  317.  
  318.  
  319.  
  320.  
  321. let count = 0;
  322. let shownUrls = [];
  323.  
  324. async function showM3U({ url, content }) {
  325.  
  326. url = new URL(url);
  327.  
  328. if(shownUrls.includes(url.href)) return;
  329.  
  330. // 解析 m3u
  331. content = content || await (await fetch(url)).text();
  332.  
  333. const parser = new m3u8Parser.Parser();
  334. parser.push(content);
  335. parser.end();
  336. const manifest = parser.manifest;
  337.  
  338. if (manifest.segments) {
  339. let duration = 0;
  340. manifest.segments.forEach((segment) => {
  341. duration += segment.duration;
  342. });
  343. manifest.duration = duration;
  344. }
  345.  
  346. let div = document.createElement("div");
  347. div.className = "m3u8-item";
  348. div.innerHTML = `
  349. <span
  350. title="${url}"
  351. style="
  352. max-width: 200px;
  353. text-overflow: ellipsis;
  354. white-space: nowrap;
  355. overflow: hidden;
  356. "
  357. >${url.pathname}</span>
  358. <span
  359. style="
  360. margin-left: 10px;
  361. flex-grow: 1;
  362. "
  363. >${manifest.duration ? `${Math.ceil(manifest.duration * 10 / 60) / 10}分钟` : manifest.playlists ? `多线(${manifest.playlists.length})` : "未知时长"}</span>
  364. <span
  365. class="download-btn"
  366. style="
  367. margin-left: 10px;
  368. cursor: pointer;
  369. ">下载</span>
  370. `;
  371.  
  372. div.querySelector(".download-btn").addEventListener("click", () => {
  373. GM_openInTab(
  374. `https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({
  375. m3u8: url.href,
  376. referer: location.href
  377. })
  378. }`,
  379. {
  380. active: true
  381. }
  382. );
  383. });
  384.  
  385. rootDiv.style.display = "block";
  386.  
  387. count++;
  388.  
  389. shownUrls.push(url.href);
  390.  
  391. bar.querySelector(".number-indicator").setAttribute("data-number", count);
  392.  
  393. wrapper.appendChild(div);
  394. }
  395.  
  396. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址