// ==UserScript== // @name ⬇️2026 LURL / MyPPT 影片下載器 // @name:zh-TW ⬇️2026 LURL / MyPPT 影片下載器 // @namespace https://github.com/cloudlin/lurl-downloader // @version 1.1.0 // @description 自動年齡驗證、自動密碼填入、一鍵下載 lurl.cc / myppt.cc 影片 // @description:zh-TW 自動年齡驗證、自動密碼填入、一鍵下載 lurl.cc / myppt.cc 影片 // @author cloudlin // @match *://lurl.cc/* // @match *://myppt.cc/* // @grant GM_download // @grant GM_addStyle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/566313/%E2%AC%87%EF%B8%8F2026%20LURL%20%20MyPPT%20%E5%BD%B1%E7%89%87%E4%B8%8B%E8%BC%89%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/566313/%E2%AC%87%EF%B8%8F2026%20LURL%20%20MyPPT%20%E5%BD%B1%E7%89%87%E4%B8%8B%E8%BC%89%E5%99%A8.meta.js // ==/UserScript== (function () { "use strict"; const VIDEO_SELECTORS = [ ".vjs-tech source", "#video source", "#my_video_html5_api source", "video source", "video[src]", "#my_video_html5_api[src]", ]; const PASSWORD_SELECTORS = ["input#password", "input#pasahaicsword"]; const AGE_BUTTON_TEXTS = ["我已年滿", "進入", "確認", "Yes", "Enter"]; const SUBMIT_SELECTORS = [ "form button", "button[type='submit']", "input[type='submit']", ]; const LOG_PREFIX = "[lurl-downloader]"; function log(...args) { console.log(LOG_PREFIX, ...args); } // --- 影片 URL 提取 --- function findVideoUrl() { for (const selector of VIDEO_SELECTORS) { const elements = document.querySelectorAll(selector); for (const el of elements) { const src = el.src || el.getAttribute("src"); if (src && src.startsWith("http")) { return src; } } } // 嘗試 Video.js player API const player = document.querySelector(".video-js"); if (player && player.player) { try { const tech = player.player.tech({ IWillNotUseThisInPlugins: true }); if (tech && tech.src_) return tech.src_; } catch (_) { // ignore } try { const src = player.player.currentSrc(); if (src) return src; } catch (_) { // ignore } } return null; } // --- 年齡驗證 --- function handleAgeVerification() { const buttons = document.querySelectorAll("button"); for (const btn of buttons) { const text = btn.textContent || ""; if (AGE_BUTTON_TEXTS.some((t) => text.includes(t)) && btn.offsetParent !== null) { log("點擊年齡驗證按鈕:", text.trim()); btn.click(); return true; } } return false; } // --- 密碼處理 --- function extractDatePassword() { const span = document.querySelector("div.col-sm-12 span.login_span"); if (!span) return null; const match = span.textContent.match(/\d{4}-(\d{2})-(\d{2})/); if (match) { return match[1] + match[2]; } return null; } function findPasswordInput() { for (const selector of PASSWORD_SELECTORS) { const el = document.querySelector(selector); if (el && el.offsetParent !== null) return el; } return null; } function submitPassword(password) { const input = findPasswordInput(); if (!input) return false; log("填入密碼:", password); // 使用 native setter 確保 React/Vue 等框架能偵測到值變化 const nativeSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, "value" ).set; nativeSetter.call(input, password); input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); // 尋找提交按鈕 for (const selector of SUBMIT_SELECTORS) { const btn = document.querySelector(selector); if (btn && btn.offsetParent !== null) { log("點擊提交按鈕:", selector); btn.click(); return true; } } // fallback: 模擬 Enter 鍵 input.dispatchEvent( new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }) ); input.dispatchEvent( new KeyboardEvent("keypress", { key: "Enter", code: "Enter", bubbles: true }) ); input.dispatchEvent( new KeyboardEvent("keyup", { key: "Enter", code: "Enter", bubbles: true }) ); return true; } function getPasswordStorageKey() { return "video-dl-pw-tried:" + location.hostname + location.pathname; } function handlePassword() { const input = findPasswordInput(); if (!input) return false; const storageKey = getPasswordStorageKey(); if (sessionStorage.getItem(storageKey)) { log("已嘗試過自動密碼,跳過(避免無限重試)"); return false; } const password = extractDatePassword(); if (password) { log("嘗試日期密碼:", password); sessionStorage.setItem(storageKey, password); submitPassword(password); return true; } return false; } // --- 下載按鈕 --- function getFilenameFromUrl(url) { try { const pathname = new URL(url).pathname; const filename = pathname.split("/").pop(); if (filename && filename.includes(".")) return filename; } catch (_) { // ignore } return "video.mp4"; } function injectDownloadButton(videoUrl) { if (document.getElementById("lurl-download-btn")) return; const filename = getFilenameFromUrl(videoUrl); const btn = document.createElement("button"); btn.id = "lurl-download-btn"; btn.textContent = "⬇ 下載影片"; btn.title = filename; btn.addEventListener("click", () => { btn.textContent = "⏳ 下載中..."; btn.disabled = true; log("開始下載:", videoUrl); log("檔名:", filename); log("Referer:", location.href); GM_download({ url: videoUrl, name: filename, headers: { Referer: location.href }, onload: () => { log("下載完成"); btn.textContent = "✅ 下載完成"; setTimeout(() => { btn.textContent = "⬇ 下載影片"; btn.disabled = false; }, 3000); }, onerror: (err) => { log("GM_download 失敗:", err); // fallback: 開新分頁讓使用者右鍵另存 btn.textContent = "⬇ 下載影片"; btn.disabled = false; window.open(videoUrl, "_blank"); }, }); }); document.body.appendChild(btn); log("下載按鈕已注入"); } // --- 狀態通知 --- function showNotification(message, duration) { let container = document.getElementById("lurl-notification"); if (!container) { container = document.createElement("div"); container.id = "lurl-notification"; document.body.appendChild(container); } container.textContent = message; container.style.display = "block"; if (duration) { setTimeout(() => { container.style.display = "none"; }, duration); } } // --- 樣式 --- GM_addStyle(` #lurl-download-btn { position: fixed; bottom: 24px; right: 24px; z-index: 999999; padding: 12px 24px; background: #2563eb; color: #fff; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transition: background 0.2s, transform 0.1s; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #lurl-download-btn:hover:not(:disabled) { background: #1d4ed8; transform: translateY(-1px); } #lurl-download-btn:active:not(:disabled) { transform: translateY(0); } #lurl-download-btn:disabled { background: #6b7280; cursor: not-allowed; } #lurl-notification { position: fixed; top: 16px; right: 16px; z-index: 999999; padding: 10px 18px; background: rgba(0, 0, 0, 0.8); color: #fff; border-radius: 6px; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: none; } `); // --- 主流程 --- function run() { log("腳本啟動"); // 第一步:處理年齡驗證 if (handleAgeVerification()) { showNotification("已自動通過年齡驗證", 2000); } // 第二步:處理密碼 if (handlePassword()) { showNotification("已自動嘗試密碼", 2000); } // 第三步:監聽影片元素出現 const videoUrl = findVideoUrl(); if (videoUrl) { log("直接找到影片 URL:", videoUrl); injectDownloadButton(videoUrl); return; } // 使用 MutationObserver 等待影片元素動態載入 log("等待影片元素載入..."); const observer = new MutationObserver(() => { const url = findVideoUrl(); if (url) { log("偵測到影片 URL:", url); observer.disconnect(); injectDownloadButton(url); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["src"], }); // 30 秒超時 setTimeout(() => { observer.disconnect(); if (!findVideoUrl()) { log("30 秒內未偵測到影片"); } }, 30000); } // 頁面載入後延遲執行,確保 DOM 穩定 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => setTimeout(run, 1500)); } else { setTimeout(run, 1500); } })();