Greasy Fork

Greasy Fork is available in English.

Arca Live 图片和视频下载器

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

// ==UserScript==
// @name         Arca Live Image and Video Downloader
// @name:zh-TW   Arca Live 圖片與影片下載器
// @name:zh-CN   Arca Live 图片和视频下载器
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Supports downloading images, GIFs, MP4s, and WEBMs from Arca Live posts (using GM_download to bypass CORS), and automatically names the files in the format: Board_PostNumber_0001~n
// @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       ChatGPT
// @match        https://arca.live/*
// @grant        GM_download
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // 延遲用的 sleep 函數(毫秒)
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

  // 等待 Arca Live 收藏按鈕出現,作為我們插入自訂按鈕的參考點
  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); // 每次等待 200 毫秒
    }
    return null;
  };

  // 收集所有圖片與影片的網址(支援 JPG、PNG、GIF、MP4、WEBM)
  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 src="..."> 影片來源
    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 href="xxx.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); // 將 Set 轉為陣列並回傳
  };

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

  // 使用 GM_download 逐一下載媒體檔案
  const downloadMedia = async (urls, button) => {
    const { board, postId } = parseBoardInfo();
    let success = 0;

    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}`; // 組合檔名

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

      await sleep(500); // 每張圖片間隔 500ms,避免過快觸發限制
    }

    // 完成後提示
    button.textContent = '✅ 下載完成';
    setTimeout(() => {
      button.disabled = false;
      button.textContent = '📥 逐張圖片下載';
    }, 4000);
  };

  // 建立「📥 逐張圖片下載」按鈕
  const createDownloadButton = () => {
    const btn = document.createElement('button');
    btn.textContent = '📥 逐張圖片下載';
    btn.className = 'btn btn-arca btn-sm float-left mr-2'; // 插入到收藏按鈕左側,float-left 並加右邊距
    btn.type = 'button';

    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);
    });

    return btn;
  };

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

    const downloadBtn = createDownloadButton();
    scrapBtn.parentElement.insertBefore(downloadBtn, scrapBtn); // 插入左側
  };

  insertButton();
})();