// ==UserScript== // @name m3u8视频侦测下载器【自动嗅探】 // @namespace https://tools.thatwind.com/ // @homepage https://tools.thatwind.com/tool/m3u8downloader // @version 1.0.1 // @description 自动检测页面m3u8视频并进行完整下载。检测到m3u8链接后会自动出现在页面右上角位置,点击下载即可跳转到m3u8下载器。 // @author allFull // @match *://*/* // @icon https://tools.thatwind.com/favicon.png // @require https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js // @connect * // @grant unsafeWindow // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @run-at document-start // @downloadURL none // ==/UserScript== (function () { 'use strict'; if (location.host === "tools.thatwind.com") { GM_addStyle("#userscript-tip{display:none !important;}"); // 对请求做代理 const _fetch = unsafeWindow.fetch; unsafeWindow.fetch = async function (...args) { try { let response = await _fetch(...args); if (response.status !== 200) throw new Error(response.status); return response; } catch (e) { // 失败请求使用代理 if (args.length == 1) { console.log(`请求代理:${args[0]}`); return await new Promise((resolve, reject) => { let referer = new URLSearchParams(location.hash.slice(1)).get("referer"); let headers = {}; if (referer) { referer = new URL(referer); headers = { "origin": referer.origin, "referer": referer.href }; } GM_xmlhttpRequest({ method: "GET", url: args[0], responseType: 'arraybuffer', headers, onload(r) { resolve({ status: r.status, headers: new Headers(r.responseHeaders.split("\n").filter(n => n).map(s => s.split(/:\s*/)).reduce((all, [a, b]) => { all[a] = b; return all; }, {})), async text() { return r.responseText; }, async arrayBuffer() { return r.response; } }); }, onerror() { reject(new Error()); } }); }); } else { throw e; } } } return; } { // 请求检测 const _fetch = unsafeWindow.fetch; unsafeWindow.fetch = function (...args) { checkUrl(args[0]); return _fetch(...args); } const _open = unsafeWindow.XMLHttpRequest.prototype.open; unsafeWindow.XMLHttpRequest.prototype.open = function (...args) { checkUrl(args[1]); return _open.apply(this, args); } } const rootDiv = document.createElement("div"); rootDiv.style = ` position: fixed; z-index: 9999999999999999; opacity: 0.9; `; rootDiv.style.display = "none"; document.documentElement.appendChild(rootDiv); const shadowDOM = rootDiv.attachShadow({ mode: 'open' }); const wrapper = document.createElement("div"); shadowDOM.appendChild(wrapper); // 指示器 const bar = document.createElement("div"); bar.style = ` text-align: right; `; bar.innerHTML = ` `; wrapper.appendChild(bar); // 样式 const style = document.createElement("style"); style.innerHTML = ` .number-indicator{ position:relative; } .number-indicator::after{ content: attr(data-number); position: absolute; bottom: 0; right: 0; color: #40a9ff; font-size: 14px; font-weight: bold; background: #000; border-radius: 10px; padding: 3px 5px; } .download-btn:hover{ text-decoration: underline; } .download-btn:active{ opacity: 0.9; } .m3u8-item{ color: white; margin-bottom: 5px; display: flex; flex-direction: row; background: black; padding: 3px 10px; border-radius: 3px; font-size: 14px; user-select: none; } [data-shown="false"] { opacity: 0.8; zoom: 0.8; } [data-shown="false"]:hover{ opacity: 1; } [data-shown="false"] .m3u8-item{ display: none; } `; wrapper.appendChild(style); const barBtn = bar.querySelector(".number-indicator"); // 关于显隐和移动 let shown = GM_getValue("shown", true); wrapper.setAttribute("data-shown", shown); let x = GM_getValue("x", 10); let y = GM_getValue("y", 10); x = Math.min(innerWidth - 50, x); y = Math.min(innerHeight - 50, y); if (x < 0) x = 0; if (y < 0) y = 0; rootDiv.style.top = `${y}px`; rootDiv.style.right = `${x}px`; barBtn.addEventListener("mousedown", e => { let startX = e.pageX; let startY = e.pageY; let moved = false; let mousemove = e => { let offsetX = e.pageX - startX; let offsetY = e.pageY - startY; if (moved || (Math.abs(offsetX) + Math.abs(offsetY)) > 5) { moved = true; rootDiv.style.top = `${y + offsetY}px`; rootDiv.style.right = `${x - offsetX}px`; } }; let mouseup = e => { let offsetX = e.pageX - startX; let offsetY = e.pageY - startY; if (moved) { x -= offsetX; y += offsetY; GM_setValue("x", x); GM_setValue("y", y); } else { shown = !shown; GM_setValue("shown", shown); wrapper.setAttribute("data-shown", shown); } removeEventListener("mousemove", mousemove); removeEventListener("mouseup", mouseup); } addEventListener("mousemove", mousemove); addEventListener("mouseup", mouseup); }); function checkUrl(url) { url = new URL(url, location.href); if (url.pathname.endsWith(".m3u8") || url.pathname.endsWith(".m3u")) { // 发现 showM3U(url); } } let count = 0; async function showM3U(url) { // 解析 m3u const content = await (await fetch(url)).text(); const parser = new m3u8Parser.Parser(); parser.push(content); parser.end(); const manifest = parser.manifest; if (manifest.segments) { let duration = 0; manifest.segments.forEach((segment) => { duration += segment.duration; }); manifest.duration = duration; } let div = document.createElement("div"); div.className = "m3u8-item"; div.innerHTML = ` ${url.pathname} ${manifest.duration ? `${Math.ceil(manifest.duration * 10 / 60) / 10}分钟` : manifest.playlists ? `多线(${manifest.playlists.length})` : "未知时长"} 下载 `; div.querySelector(".download-btn").addEventListener("click", () => { GM_openInTab( `https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({ m3u8: url.href, referer: location.href }) }`, { active: true } ); }); rootDiv.style.display = "block"; count++; bar.querySelector(".number-indicator").setAttribute("data-number", count); wrapper.appendChild(div); } })();