Greasy Fork

Greasy Fork is available in English.

Arca Live 图片和视频下载器

支援下载 Arca Live 贴文中的图片、GIF、MP4、WEBM(使用 GM_download 绕过 CORS)并自动命名为「板块_编号_0001~n」格式,快速下载、逐一下载两种模式。

当前为 2025-10-06 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Arca Live Image and Video Downloader
// @name:zh-TW   Arca Live 圖片與影片下載器
// @name:zh-CN   Arca Live 图片和视频下载器
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Supports downloading images, GIFs, MP4s, and WEBMs from Arca Live posts (using GM_download to bypass CORS), with automatic filename formatting as "Board_PostID_0001~n". Offers both fast download and sequential download modes.
// @description:zh-TW 支援下載 Arca Live 貼文中的圖片、GIF、MP4、WEBM(使用 GM_download 繞過 CORS)並自動命名為「板塊_編號_0001~n」格式,快速下載、逐一下載兩種模式。
// @description:zh-CN 支援下载 Arca Live 贴文中的图片、GIF、MP4、WEBM(使用 GM_download 绕过 CORS)并自动命名为「板块_编号_0001~n」格式,快速下载、逐一下载两种模式。
// @author       Hzbrrbmin + ChatGPT
// @match        https://arca.live/*
// @grant        GM_download
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // 延遲函式,方便等待非同步流程
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

  // 等待收藏按鈕出現,作為插入下載按鈕的參考點,最多等待 10 秒(50 次 * 200ms)
  const waitForScrapButton = async () => {
    for (let i = 0; i < 50; i++) {
      const scrapBtn = document.querySelector('form#scrapForm > button.scrap-btn');
      if (scrapBtn) return scrapBtn;
      await sleep(200);
    }
    return null;
  };

  // 從網址中解析板塊名稱與貼文 ID,例如 /b/umamusume/141070493
  const parseBoardInfo = () => {
    const match = location.pathname.match(/^\/b\/([^/]+)\/(\d+)/);
    if (!match) return { board: 'unknown', postId: 'unknown' };
    return { board: match[1], postId: match[2] };
  };

  // 收集貼文中所有可下載的媒體網址(圖片與影片)
  const collectMediaUrls = () => {
    const urls = new Set();

    // <img> 標籤圖片(優先取 data-src)
    document.querySelectorAll('.article-body img').forEach(img => {
      const src = img.getAttribute('data-src') || img.src;
      if (src) urls.add(src);
    });

    // <video><source> 影片來源
    document.querySelectorAll('.article-body video source').forEach(source => {
      if (source.src) urls.add(source.src);
    });

    // <video> 直接 src 屬性
    document.querySelectorAll('.article-body video').forEach(video => {
      if (video.src) urls.add(video.src);
    });

    // <a> 超連結,篩選 gif / mp4 / webm 檔案
    document.querySelectorAll('.article-body a[href]').forEach(a => {
      const href = a.href;
      if (/\.(gif|mp4|webm)(\?.*)?$/i.test(href)) {
        urls.add(href);
      }
    });

    return Array.from(urls);
  };

  // 建立快速下載模式切換按鈕,懸浮在主下載按鈕右側
  // 傳入主下載按鈕元素,包裝成相對定位容器,方便同時放入兩個元素
  const createFloatingFastToggle = (relativeToButton) => {
    const toggle = document.createElement('div');
    toggle.textContent = '⚡ 快速下載模式:❌';
    toggle.style.position = 'absolute';
    toggle.style.left = '100%'; // 緊貼主按鈕右側
    toggle.style.top = '0';
    toggle.style.marginLeft = '10px'; // 按鈕間隔
    toggle.style.padding = '4px 8px';
    toggle.style.backgroundColor = '#343a40';
    toggle.style.color = '#fff';
    toggle.style.borderRadius = '6px';
    toggle.style.whiteSpace = 'nowrap'; // 防止換行
    toggle.style.fontSize = '12px';
    toggle.style.cursor = 'pointer';
    toggle.style.userSelect = 'none'; // 避免文字被選取
    toggle.style.zIndex = '999'; // 置頂

    let fastMode = false; // 內部狀態,預設關閉

    // 點擊切換開關文字與狀態
    toggle.addEventListener('click', () => {
      fastMode = !fastMode;
      toggle.textContent = `⚡ 快速下載模式:${fastMode ? '✅' : '❌'}`;
    });

    // 建立一個相對定位的容器,包含主按鈕與切換按鈕
    const wrapper = document.createElement('div');
    wrapper.style.position = 'relative';
    wrapper.style.display = 'inline-block';
    wrapper.appendChild(relativeToButton);
    wrapper.appendChild(toggle);

    // 回傳容器與取得目前開關狀態的函式
    return { wrapper, getFastMode: () => fastMode };
  };

  // 下載媒體函式,支援快速模式(並行下載)與逐一下載
  // fastMode 為布林值,true 使用快速下載
  const downloadMedia = async (urls, button, fastMode) => {
    const { board, postId } = parseBoardInfo();
    let success = 0;

    if (fastMode) {
      // 快速模式:多個下載任務同時啟動,但每個任務之間仍維持 100ms 間隔
      const downloadTasks = urls.map((url, i) => {
        // 解析副檔名,若無則用 bin
        const ext = url.split('.').pop().split('?')[0].split('#')[0] || 'bin';
        // 檔名格式:板塊_貼文編號_四位數流水號.ext
        const filename = `${board}_${postId}_${String(i + 1).padStart(4, '0')}.${ext}`;

        return new Promise((resolve) => {
          try {
            GM_download({
              url,
              name: filename,
              saveAs: false,
              onload: () => {
                success++;
                button.textContent = `下載中 (${success}/${urls.length})`;
                resolve();
              },
              onerror: (err) => {
                console.warn(`❌ 無法下載: ${url}`, err);
                resolve();
              }
            });
          } catch (e) {
            console.error(`❌ GM_download 錯誤: ${url}`, e);
            resolve();
          }
        }).then(() => sleep(100)); // 保持間隔避免同時大量請求
      });

      await Promise.all(downloadTasks);
      button.textContent = '✅ 下載完成';

    } else {
      // 逐一下載模式:等待一張下載完成後才下載下一張
      for (let i = 0; i < urls.length; i++) {
        const url = urls[i];
        const ext = url.split('.').pop().split('?')[0].split('#')[0] || 'bin';
        const filename = `${board}_${postId}_${String(i + 1).padStart(4, '0')}.${ext}`;

        await new Promise((resolve) => {
          try {
            GM_download({
              url,
              name: filename,
              saveAs: false,
              onload: () => {
                success++;
                button.textContent = `下載中 (${success}/${urls.length})`;
                resolve();
              },
              onerror: (err) => {
                console.warn(`❌ 無法下載: ${url}`, err);
                resolve();
              }
            });
          } catch (e) {
            console.error(`❌ GM_download 錯誤: ${url}`, e);
            resolve();
          }
        });
      }

      button.textContent = '✅ 下載完成';
    }

    // 等待 30 秒後重置按鈕狀態(避免重複點擊衝突)
    setTimeout(() => {
      button.disabled = false;
      button.textContent = '📥 逐張圖片下載';
    }, 30000);
  };

  // 建立下載按鈕與快速模式切換按鈕容器
  const createDownloadButtonWithToggle = () => {
    // 下載按鈕
    const btn = document.createElement('button');
    btn.textContent = '📥 逐張圖片下載';
    btn.className = 'btn btn-arca btn-sm float-left mr-2';
    btn.type = 'button';

    // 產生包裝容器與快速模式取得函式
    const { wrapper, getFastMode } = createFloatingFastToggle(btn);

    // 按下下載按鈕的事件
    btn.addEventListener('click', async () => {
      btn.disabled = true;
      btn.textContent = '🔄 收集媒體中...';

      // 收集所有媒體網址
      const urls = collectMediaUrls();
      if (urls.length === 0) {
        alert('⚠️ 找不到任何圖片或影片');
        btn.disabled = false;
        btn.textContent = '📥 逐張圖片下載';
        return;
      }

      // 依快速模式開關狀態下載
      await downloadMedia(urls, btn, getFastMode());
    });

    return wrapper;
  };

  // 插入按鈕組合到收藏按鈕左側
  const insertButton = async () => {
    const scrapBtn = await waitForScrapButton();
    if (!scrapBtn) return;

    const downloadWrapper = createDownloadButtonWithToggle();
    scrapBtn.parentElement.insertBefore(downloadWrapper, scrapBtn);
  };

  // 啟動腳本
  insertButton();

})();