// ==UserScript== // @name m3u8视频侦测下载,调用本地potplayer播放器,调用以后就可以实现VR格式的视频直接3D转2D // @name:zh-CN m3u8视频侦测下载器 // @name:zh-TW m3u8視頻偵測下載器 // @name:en M3U8 Video Detector and Downloader // @version 1.5.2 // @description 自动检测页面m3u8视频并进行完整下载。修复了URL编码问题,确保PotPlayer正确调用。 // @description:zh-CN 自动检测页面m3u8视频并进行完整下载。修复了URL编码问题,确保PotPlayer正确调用。 // @description:zh-TW 自動檢測頁面m3u8視頻並進行完整下載。修復了URL編碼問題,確保PotPlayer正確調用。 // @description:en Automatically detect the m3u8 video of the page and download it completely. Fixed URL encoding issue to ensure PotPlayer works correctly. // @icon https://tools.thatwind.com/favicon.png // @author allFull // @namespace https://tools.thatwind.com/ // @homepage https://tools.thatwind.com/tool/m3u8downloader // @match *://*/* // @exclude *://www.diancigaoshou.com/* // @require https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js // @connect * // @grant unsafeWindow // @grant GM_openInTab // @grant GM.openInTab // @grant GM_getValue // @grant GM.getValue // @grant GM_setValue // @grant GM.setValue // @grant GM_deleteValue // @grant GM.deleteValue // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_download // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/549666/m3u8%E8%A7%86%E9%A2%91%E4%BE%A6%E6%B5%8B%E4%B8%8B%E8%BD%BD%EF%BC%8C%E8%B0%83%E7%94%A8%E6%9C%AC%E5%9C%B0potplayer%E6%92%AD%E6%94%BE%E5%99%A8%EF%BC%8C%E8%B0%83%E7%94%A8%E4%BB%A5%E5%90%8E%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%AE%9E%E7%8E%B0VR%E6%A0%BC%E5%BC%8F%E7%9A%84%E8%A7%86%E9%A2%91%E7%9B%B4%E6%8E%A53D%E8%BD%AC2D.user.js // @updateURL https://update.greasyfork.icu/scripts/549666/m3u8%E8%A7%86%E9%A2%91%E4%BE%A6%E6%B5%8B%E4%B8%8B%E8%BD%BD%EF%BC%8C%E8%B0%83%E7%94%A8%E6%9C%AC%E5%9C%B0potplayer%E6%92%AD%E6%94%BE%E5%99%A8%EF%BC%8C%E8%B0%83%E7%94%A8%E4%BB%A5%E5%90%8E%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%AE%9E%E7%8E%B0VR%E6%A0%BC%E5%BC%8F%E7%9A%84%E8%A7%86%E9%A2%91%E7%9B%B4%E6%8E%A53D%E8%BD%AC2D.meta.js // ==/UserScript== (function () { 'use strict'; const mgmapi = { addStyle(s) { let style = document.createElement("style"); style.innerHTML = s; document.documentElement.appendChild(style); }, async getValue(name, defaultVal) { return await ((typeof GM_getValue === "function") ? GM_getValue : GM.getValue)(name, defaultVal); }, async setValue(name, value) { return await ((typeof GM_setValue === "function") ? GM_setValue : GM.setValue)(name, value); }, async deleteValue(name) { return await ((typeof GM_deleteValue === "function") ? GM_deleteValue : GM.deleteValue)(name); }, openInTab(url, open_in_background = false) { return ((typeof GM_openInTab === "function") ? GM_openInTab : GM.openInTab)(url, open_in_background); }, xmlHttpRequest(details) { return ((typeof GM_xmlhttpRequest === "function") ? GM_xmlhttpRequest : GM.xmlHttpRequest)(details); }, download(details) { return this.openInTab(details.url); }, copyText(text) { copyTextToClipboard(text); function copyTextToClipboard(text) { var copyFrom = document.createElement("textarea"); copyFrom.textContent = text; document.body.appendChild(copyFrom); copyFrom.select(); document.execCommand('copy'); copyFrom.blur(); document.body.removeChild(copyFrom); } }, message(text, disappearTime = 5000) { const id = "f8243rd238-gm-message-panel"; let p = document.querySelector(`#${id}`); if (!p) { p = document.createElement("div"); p.id = id; p.style = ` position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; align-items: end; z-index: 999999999999999; `; (document.body || document.documentElement).appendChild(p); } let mdiv = document.createElement("div"); mdiv.innerText = text; mdiv.style = ` padding: 3px 8px; border-radius: 5px; background: black; box-shadow: #000 1px 2px 5px; margin-top: 10px; font-size: small; color: #fff; text-align: right; `; p.appendChild(mdiv); setTimeout(() => { p.removeChild(mdiv); }, disappearTime); } }; // 播放器配置 - 修复URL编码问题 const players = { "vlc": { name: "VLC Player", protocol: "vlc://", // VLC需要编码的URL format: (url) => `vlc://${encodeURIComponent(url)}` }, "potplayer": { name: "PotPlayer", protocol: "potplayer://", // PotPlayer需要未编码的原始URL format: (url) => `potplayer://${url}` }, "mpc-hc": { name: "MPC-HC", protocol: "mpc-hc://", format: (url) => `mpc-hc://open/file?url=${encodeURIComponent(url)}` }, "mpv": { name: "MPV", protocol: "mpv://", format: (url) => `mpv://${url}` } }; // 获取默认播放器 - 默认使用PotPlayer async function getDefaultPlayer() { const defaultPlayer = await mgmapi.getValue("defaultPlayer", "potplayer"); return players[defaultPlayer] ? defaultPlayer : "potplayer"; } // 设置默认播放器 async function setDefaultPlayer(playerId) { if (players[playerId]) { await mgmapi.setValue("defaultPlayer", playerId); return true; } return false; } // 调用本地播放器 - 确保URL正确处理 function launchLocalPlayer(url) { // 确保URL是解码后的原始格式 let decodedUrl = url; try { // 检查是否已编码,如果是则解码 if (decodedUrl !== decodeURIComponent(decodedUrl)) { decodedUrl = decodeURIComponent(decodedUrl); } } catch (e) { console.log("URL无需解码或解码失败,使用原始URL:", e); } getDefaultPlayer().then(defaultPlayer => { try { const player = players[defaultPlayer]; const playerUrl = player.format(decodedUrl); // 创建一个隐藏的链接并点击它来调用本地播放器 const link = document.createElement('a'); link.href = playerUrl; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); mgmapi.message(`正在使用${player.name}播放...\nPlaying with ${player.name}...`, 3000); } catch (e) { mgmapi.message(`调用播放器失败,请确保已安装${players[defaultPlayer].name}\nFailed to launch player. Please ensure ${players[defaultPlayer].name} is installed.`, 5000); console.error("播放器调用失败:", e); } }); } // 添加播放器设置按钮 function addPlayerSettings() { const settingsDiv = document.createElement("div"); settingsDiv.className = "player-settings"; settingsDiv.style = ` color: white; padding: 5px 10px; background: #333; border-radius: 3px; font-size: 12px; margin-bottom: 5px; `; let settingsHtml = "默认播放器 (Default Player): "; Object.keys(players).forEach(playerId => { settingsHtml += ` ${players[playerId].name} `; }); settingsDiv.innerHTML = settingsHtml; wrapper.appendChild(settingsDiv); // 设置默认选中状态 getDefaultPlayer().then(defaultPlayer => { document.querySelector(`.player-option.${defaultPlayer}`).style.background = "#666"; }); // 绑定播放器选择事件 document.querySelectorAll(".player-option").forEach(option => { option.addEventListener("click", function() { const playerId = this.getAttribute("data-player"); setDefaultPlayer(playerId).then(success => { if (success) { document.querySelectorAll(".player-option").forEach(opt => { opt.style.background = ""; }); this.style.background = "#666"; mgmapi.message(`默认播放器已设置为${players[playerId].name}\nDefault player set to ${players[playerId].name}`, 2000); } }); }); }); } if (location.host === "tools.thatwind.com" || location.host === "localhost:3000") { mgmapi.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 }; } mgmapi.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; } // iframe 信息交流 window.addEventListener("message", async (e) => { if (e.data === "3j4t9uj349-gm-get-title") { let name = `top-title-${Date.now()}`; await mgmapi.setValue(name, document.title); e.source.postMessage(`3j4t9uj349-gm-top-title-name:${name}`, "*"); } }); function getTopTitle() { return new Promise(resolve => { window.addEventListener("message", async function l(e) { if (typeof e.data === "string") { if (e.data.startsWith("3j4t9uj349-gm-top-title-name:")) { let name = e.data.slice("3j4t9uj349-gm-top-title-name:".length); await new Promise(r => setTimeout(r, 5)); resolve(await mgmapi.getValue(name)); mgmapi.deleteValue(name); window.removeEventListener("message", l); } } }); window.top.postMessage("3j4t9uj349-gm-get-title", "*"); }); } { // 请求检测 const _r_text = unsafeWindow.Response.prototype.text; unsafeWindow.Response.prototype.text = function () { return new Promise((resolve, reject) => { _r_text.call(this).then((text) => { resolve(text); if (checkContent(text)) doM3U({ url: this.url, content: text }); }).catch(reject); }); } const _open = unsafeWindow.XMLHttpRequest.prototype.open; unsafeWindow.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", () => { try { let content = this.responseText; if (checkContent(content)) doM3U({ url: args[1], content }); } catch { } }); return _open.apply(this, args); } function checkUrl(url) { url = new URL(url, location.href); if (url.pathname.endsWith(".m3u8") || url.pathname.endsWith(".m3u")) { return true; } } function checkContent(content) { if (content.trim().startsWith("#EXTM3U")) { return true; } } // 检查纯视频 setInterval(doVideos, 1000); } 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); // 添加播放器设置 addPlayerSettings(); // 样式 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; } .copy-link:active{ color: #ccc; } .download-btn:hover, .local-play-btn:hover{ text-decoration: underline; } .download-btn:active, .local-play-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; align-items: center; flex-wrap: wrap; } [data-shown="false"] { opacity: 0.8; zoom: 0.8; } [data-shown="false"]:hover{ opacity: 1; } [data-shown="false"] .m3u8-item, [data-shown="false"] .player-settings { display: none; } .local-play-btn { margin-left: 10px; cursor: pointer; color: #40a9ff; white-space: nowrap; } .player-settings { user-select: none; } .player-option:hover { background-color: #555; } .player-option.potplayer { background-color: #666; } `; wrapper.appendChild(style); const barBtn = bar.querySelector(".number-indicator"); // 关于显隐和移动 (async function () { let shown = await mgmapi.getValue("shown", true); wrapper.setAttribute("data-shown", shown); let x = await mgmapi.getValue("x", 10); let y = await mgmapi.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; mgmapi.setValue("x", x); mgmapi.setValue("y", y); } else { shown = !shown; mgmapi.setValue("shown", shown); wrapper.setAttribute("data-shown", shown); } removeEventListener("mousemove", mousemove); removeEventListener("mouseup", mouseup); } addEventListener("mousemove", mousemove); addEventListener("mouseup", mouseup); }); })(); let count = 0; let shownUrls = []; function doVideos() { for (let v of Array.from(document.querySelectorAll("video"))) { if (v.duration && v.src && v.src.startsWith("http") && (!shownUrls.includes(v.src))) { const src = v.src; shownUrls.push(src); showVideo({ type: "video", url: new URL(src), duration: `${Math.ceil(v.duration * 10 / 60) / 10} mins`, download() { const details = { url: src, name: (() => { let name = new URL(src).pathname.split("/").slice(-1)[0]; if (!/\.\w+$/.test(name)) { if (name.match(/^\s*$/)) name = Date.now(); name = name + ".mp4"; } return name; })(), headers: { origin: location.origin }, onerror(e) { mgmapi.openInTab(src); } }; mgmapi.download(details); }, playLocally() { launchLocalPlayer(src); } }) } } } async function doM3U({ url, content }) { url = new URL(url); if (shownUrls.includes(url.href)) return; // 解析 m3u content = 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; } showVideo({ type: "m3u8", url, duration: manifest.duration ? `${Math.ceil(manifest.duration * 10 / 60) / 10} mins` : manifest.playlists ? `多(Multi)(${manifest.playlists.length})` : "未知(unknown)", async download() { mgmapi.openInTab( `https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({ m3u8: url.href, referer: location.href, filename: (await getTopTitle()) || "" })}` ); }, playLocally() { launchLocalPlayer(url.href); } }) } async function showVideo({ type, url, duration, download, playLocally }) { let div = document.createElement("div"); div.className = "m3u8-item"; div.innerHTML = ` ${type} ${url.pathname} ${duration} 本地播放(Local) 下载(Download) `; div.querySelector(".copy-link").addEventListener("click", () => { mgmapi.copyText(url.href); mgmapi.message("已复制链接 (link copied)", 2000); }); div.querySelector(".download-btn").addEventListener("click", download); div.querySelector(".local-play-btn").addEventListener("click", playLocally); rootDiv.style.display = "block"; count++; shownUrls.push(url.href); bar.querySelector(".number-indicator").setAttribute("data-number", count); wrapper.appendChild(div); } })(); (function () { 'use strict'; const reg = /magnet:\?xt=urn:btih:\w{10,}([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; let l = navigator.language || "en"; if (l.startsWith("en-")) l = "en"; else if (l.startsWith("zh-")) l = "zh-CN"; else l = "en"; const T = { "en": { play: "Play", localPlay: "Local Play (PotPlayer)" }, "zh-CN": { play: '播放', localPlay: '本地播放 (PotPlayer)' } }[l]; // 播放器配置 - 磁力链接专用,修复URL编码 const magnetPlayers = { "vlc": { name: "VLC Player", protocol: "vlc://", format: (url) => `vlc://${encodeURIComponent(url)}` }, "potplayer": { name: "PotPlayer", protocol: "potplayer://", // PotPlayer磁力链接也使用未编码的URL format: (url) => `potplayer://${url}` } }; // 获取默认磁力链接播放器 async function getMagnetDefaultPlayer() { const defaultPlayer = await GM.getValue("magnetDefaultPlayer", "potplayer"); return magnetPlayers[defaultPlayer] ? defaultPlayer : "potplayer"; } // 调用本地播放器播放磁力链接 - 修复URL编码问题 function launchMagnetLocalPlayer(url) { // 确保URL解码 let decodedUrl = url; try { if (decodedUrl !== decodeURIComponent(decodedUrl)) { decodedUrl = decodeURIComponent(decodedUrl); } } catch (e) { console.log("磁力链接URL无需解码或解码失败:", e); } getMagnetDefaultPlayer().then(defaultPlayer => { try { const player = magnetPlayers[defaultPlayer]; const playerUrl = player.format(decodedUrl); const link = document.createElement('a'); link.href = playerUrl; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); const messageDiv = document.createElement('div'); messageDiv.textContent = `正在使用PotPlayer播放...`; messageDiv.style = ` position: fixed; bottom: 20px; right: 20px; padding: 8px 12px; background: #333; color: white; border-radius: 4px; z-index: 999999; `; document.body.appendChild(messageDiv); setTimeout(() => { messageDiv.remove(); }, 3000); } catch (e) { console.error("磁力链接播放器调用失败:", e); const errorDiv = document.createElement('div'); errorDiv.textContent = `调用PotPlayer失败,请确保已安装PotPlayer并正确配置`; errorDiv.style = ` position: fixed; bottom: 20px; right: 20px; padding: 8px 12px; background: #ff4444; color: white; border-radius: 4px; z-index: 999999; `; document.body.appendChild(errorDiv); setTimeout(() => { errorDiv.remove(); }, 5000); } }); } whenDOMReady(() => { addStyle(` button[data-wtmzjk-mag-url]{ all: initial; border: none; outline: none; background: none; background: #08a6f7; margin: 2px 8px; border-radius: 3px; color: white; cursor: pointer; display: inline-flex; height: 1.6em; padding: 0 .8em; align-items: center; justify-content: center; transition: background .15s; text-decoration: none; border-radius: 0.8em; font-size: small; } button[data-wtmzjk-mag-local]{ all: initial; border: none; outline: none; background: none; background: #4CAF50; margin: 2px 8px; border-radius: 3px; color: white; cursor: pointer; display: inline-flex; height: 1.6em; padding: 0 .8em; align-items: center; justify-content: center; transition: background .15s; text-decoration: none; border-radius: 0.8em; font-size: small; } button[data-wtmzjk-mag-url]>svg, button[data-wtmzjk-mag-local]>svg{ height: 60%; fill: white; pointer-events: none; } button[data-wtmzjk-mag-url]:hover{ background: #39b9f9; } button[data-wtmzjk-mag-local]:hover{ background: #45a049; } button[data-wtmzjk-mag-url]:active{ background: #0797df; } button[data-wtmzjk-mag-local]:active{ background: #3d8b40; } button[data-wtmzjk-mag-url]>span, button[data-wtmzjk-mag-local]>span{ pointer-events: none; font-size: small;margin-right: .5em;font-weight:bold;color:white !important; } `); window.addEventListener("click", onEvents, true); window.addEventListener("mousedown", onEvents, true); window.addEventListener("mouseup", onEvents, true); watchBodyChange(work); }); function onEvents(e) { if (e.target.hasAttribute('data-wtmzjk-mag-url')) { e.preventDefault(); e.stopPropagation(); if (e.type == "click") { let a = document.createElement('a'); a.href = 'https://www.diancigaoshou.com/#' + new URLSearchParams({ url: e.target.getAttribute('data-wtmzjk-mag-url') }); a.target = "_blank"; a.click(); } } else if (e.target.hasAttribute('data-wtmzjk-mag-local')) { e.preventDefault(); e.stopPropagation(); if (e.type == "click") { launchMagnetLocalPlayer(e.target.getAttribute('data-wtmzjk-mag-local')); } } } function createWatchButton(url, isForPlain = false) { let onlineButton = document.createElement("button"); onlineButton.setAttribute('data-wtmzjk-mag-url', url); if (isForPlain) onlineButton.setAttribute('data-wtmzjk-button-for-plain', ''); onlineButton.innerHTML = `${T.play}`; let localButton = document.createElement("button"); localButton.setAttribute('data-wtmzjk-mag-local', url); if (isForPlain) localButton.setAttribute('data-wtmzjk-button-for-plain', ''); localButton.innerHTML = `${T.localPlay}`; let container = document.createElement("span"); container.appendChild(onlineButton); container.appendChild(localButton); return container; } function hasPlainMagUrlThatNotHandled() { let m = document.body.textContent.match(new RegExp(reg, 'g')); return document.querySelectorAll(`[data-wtmzjk-button-for-plain]`).length != (m ? m.length : 0); } function work() { if (!document.body) return; if (hasPlainMagUrlThatNotHandled()) { for (let node of getAllTextNodes(document.body)) { if (node.nextSibling && node.nextSibling.hasAttribute && node.nextSibling.hasAttribute('data-wtmzjk-mag-url')) continue; let text = node.nodeValue; if (!reg.test(text)) continue; let match = text.match(reg); if (match) { let url = match[0]; let p = node.parentNode; p.insertBefore(document.createTextNode(text.slice(0, match.index + url.length)), node); p.insertBefore(createWatchButton(url, true), node); p.insertBefore(document.createTextNode(text.slice(match.index + url.length)), node); p.removeChild(node); } } } for (let a of Array.from(document.querySelectorAll( ['href', 'value', 'data-clipboard-text', 'data-value', 'title', 'alt', 'data-url', 'data-magnet', 'data-copy'].map(n => `[${n}*="magnet:?xt=urn:btih:"]`).join(',') ))) { if (a.nextSibling && a.nextSibling.hasAttribute && a.nextSibling.hasAttribute('data-wtmzjk-mag-url')) continue; if (reg.test(a.textContent)) continue; for (let attr of a.getAttributeNames()) { let val = a.getAttribute(attr); if (!reg.test(val)) continue; let url = val.match(reg)[0]; a.parentNode.insertBefore(createWatchButton(url), a.nextSibling); } } } function watchBodyChange(onchange) { let timeout; let observer = new MutationObserver(() => { if (!timeout) { timeout = setTimeout(() => { timeout = null; onchange(); }, 200); } }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true }); } function getAllTextNodes(parent) { var re = []; if (["STYLE", "SCRIPT", "BASE", "COMMAND", "LINK", "META", "TITLE", "XTRANS-TXT", "XTRANS-TXT-GROUP", "XTRANS-POPUP"].includes(parent.tagName)) return re; for (let node of parent.childNodes) { if (node.childNodes.length) re = re.concat(getAllTextNodes(node)); else if (Text.prototype.isPrototypeOf(node) && (!node.nodeValue.match(/^\s*$/))) re.push(node); } return re; } function whenDOMReady(f) { if (document.body) f(); else window.addEventListener("DOMContentLoaded", f); } function addStyle(s) { let style = document.createElement("style"); style.innerHTML = s; document.documentElement.appendChild(style); } })();