Greasy Fork

Greasy Fork is available in English.

⬇️2026 LURL / MyPPT 影片下載器

自動年齡驗證、自動密碼填入、一鍵下載 lurl.cc / myppt.cc 影片

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/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);
  }
})();