fantia-image-downloader

Fantiaに投稿された画像をzipファイルでダウンロードするシンプルなスクリプトです。

目前為 2023-01-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name               fantia-image-downloader
// @name:en            fantia-image-downloader
// @name:zh-CN         fantia-image-downloader
// @namespace          https://fantia.jp/
// @version            0.1.0
// @description        Fantiaに投稿された画像をzipファイルでダウンロードするシンプルなスクリプトです。
// @description:en     Download all images posted to Fantia at once for each post.
// @description:zh-CN  一个简单的脚本,可以将Fantia上发布的图片下载成一个压缩文件。
// @author             ame-chan
// @match              https://fantia.jp/posts/*
// @icon               https://fantia.jp/assets/customers/favicon-32x32-8ab6e1f6c630503f280adca20d089646e0ea67559d5696bb3b9f34469e15c168.png
// @grant              none
// @license            MIT
// @require            https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.js
// ==/UserScript==
(() => {
  const i18n = {
    'en': {
      'noname': 'untitled',
      'initialButtonName': 'Image Download',
      'progressDownloadImages': 'Downloading images',
      'createZipFile': 'Compressed file being created',
    },
    'zh': {
      'noname': '无标题',
      'initialButtonName': '图片下载',
      'progressDownloadImages': '正在下载的图像',
      'createZipFile': '正在创建的压缩文件',
    },
    'ja': {
      'noname': '無題',
      'initialButtonName': '画像ダウンロード',
      'progressDownloadImages': '画像ダウンロード中',
      'createZipFile': '圧縮ファイル作成中',
    },
  };
  const lang = (() => {
    if (/^ja/.test(navigator.language)) {
      return i18n['ja'];
    } else if (/^zh/.test(navigator.language)) {
      return i18n['zh'];
    }
    return i18n['en'];
  })();
  const getDate = () => {
    const getNowDate = () => {
      const date = new Date();
      const yyyy = date.getFullYear();
      const mm = String(date.getMonth() + 1).padStart(2, '0');
      const dd = String(date.getDate()).padStart(2, '0');
      return `${yyyy}${mm}${dd}`;
    };
    const postDate = document.querySelector('.post-header .post-date');
    if (postDate === null) {
      return getNowDate();
    }
    const [date] = postDate.textContent?.split(' ').filter(Boolean) || [];
    return date.replace(/\//g, '');
  };
  const getPostContent = () => {
    const contentElms = document.querySelectorAll('.post-content.ng-scope');
    const postData = [];
    for (const contentElm of contentElms) {
      const contentData = {
        title: '',
        imagePaths: [],
      };
      const titleElm = contentElm.querySelector('.post-content-title');
      if (titleElm && titleElm.textContent) {
        contentData.title = titleElm.textContent === '' ? lang['noname'] : titleElm.textContent;
      }
      const imageElms = contentElm.querySelectorAll('.image-module');
      for (const imageElm of imageElms) {
        const image = imageElm.querySelector('img[src]');
        if (!image) continue;
        const [imgId = false] = image.src.match(/\/[0-9]{8}\//) || [];
        if (imgId) {
          contentData.imagePaths.push(imgId.replace(/\//g, ''));
        }
      }
      postData.push(contentData);
    }
    return postData;
  };
  const getImageFormat = (arrayBuffer) => {
    const arr = new Uint8Array(arrayBuffer).subarray(0, 4);
    let header = '';
    for (let i = 0; i < arr.length; i++) {
      header += arr[i].toString(16);
    }
    if (/^89504e47/.test(header)) {
      return 'png';
    } else if (/^47494638/.test(header)) {
      return 'gif';
    } else if (/^424d/.test(header)) {
      return 'bmp';
    } else if (/^ffd8ff/.test(header)) {
      return 'jpg';
    }
    return '';
  };
  const execDownload = async () => {
    try {
      // 新しいzipファイルを作成
      const zip = new window.JSZip();
      const postContents = getPostContent();
      const postContentsLength = postContents.length;
      const postId = location.pathname.split('/').filter(Boolean).pop();
      const baseURL = `https://fantia.jp/posts/${postId}/post_content_photo/`;
      const changeProgress = (text, percentage) => {
        const buttonElm = document.querySelector('#fantiaDLbutton');
        if (!buttonElm) return;
        if (!buttonElm.classList.contains('is-disabled')) {
          buttonElm.classList.add('is-disabled');
        }
        buttonElm.textContent = `${text} ... ${percentage}%`;
      };
      let totalFiles = 0;
      let progress = [];
      for (let i = 0; i < postContentsLength; i++) {
        const content = postContents[i];
        totalFiles += content.imagePaths.length;
      }
      // 画像のURLごとに処理を実行
      await Promise.all(
        postContents.map(async (data, index) => {
          const imageLength = data.imagePaths.length;
          // 画像のバイナリデータを取得
          for (let i = 0; i < imageLength; i++) {
            progress.push(true);
            const percentage = ((100 / totalFiles) * progress.length).toFixed(2);
            changeProgress(lang['progressDownloadImages'], percentage);
            const imagePath = data.imagePaths[i];
            const response = await (await fetch(baseURL + imagePath)).text();
            const dom = new DOMParser();
            const html = dom.parseFromString(response, 'text/html');
            const originalImage = html.querySelector('img[src*="cc.fantia.jp"]');
            if (originalImage) {
              const imageData = await fetch(originalImage.src);
              const binaryData = await imageData.arrayBuffer();
              const ext = getImageFormat(binaryData);
              const fileName = `${data.title}_${index}_${i}.${ext}`;
              zip.file(fileName, binaryData, {
                binary: true,
              });
            }
          }
        }),
      );
      const postTitleElm = document.querySelector('h1.post-title');
      const title = postTitleElm?.textContent || lang['noname'];
      // zipファイルをダウンロード
      zip
        .generateAsync(
          {
            type: 'blob',
          },
          (metadata) => {
            const percentage = metadata.percent.toFixed(2);
            changeProgress(lang['createZipFile'], percentage);
          },
        )
        .then((content) => {
          const link = document.createElement('a');
          link.href = URL.createObjectURL(content);
          link.download = `[${getDate()}] ${title}.zip`;
          link.click();
          // ボタン初期化
          const buttonElm = document.querySelector('#fantiaDLbutton');
          if (!buttonElm) return;
          buttonElm.classList.remove('is-disabled');
          buttonElm.textContent = lang['initialButtonName'];
        });
    } catch (e) {
      alert(e);
      console.error(e);
    }
  };
  const buttonStyle = `<style>
  .userjs-downloadBtn {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 24px;
  }
  .userjs-downloadBtn__button {
    appearance: none;
    margin: 0;
    padding: 8px 16px;
    font-size: 16px;
    color: #fff;
    background-color: #00d1b2;
    border: 1px solid #00d1b2;
    border-radius: 4px;
  }
  .userjs-downloadBtn__button.is-disabled {
    pointer-events: none;
    user-select: none;
    color: #aaa;
    background-color: #e0e0e0;
    border-color: #e0e0e0;
  }
  </style>`;
  const createDownloadButton = () => {
    const postMainThumb = document.querySelector('.the-post .post-thumbnail');
    const buttonHTML = `<div class="userjs-downloadBtn">
      <button type="button" id="fantiaDLbutton" class="userjs-downloadBtn__button">${lang['initialButtonName']}</button>
    </div>`;
    document.head.insertAdjacentHTML('afterbegin', buttonStyle);
    postMainThumb?.insertAdjacentHTML('beforebegin', buttonHTML);
    document.querySelector('#fantiaDLbutton')?.addEventListener('click', () => {
      void execDownload();
    });
  };
  const observeMainBlock = () => {
    const mainElm = document.querySelector('#main');
    const findPostMainThumb = () => document.querySelector('.the-post .post-thumbnail') !== null;
    if (!mainElm) {
      return console.warn('mainElm not found');
    }
    const observer = new MutationObserver((_, observer) => {
      if (findPostMainThumb()) {
        const postContents = getPostContent();
        if (postContents.length) {
          createDownloadButton();
        } else {
          console.warn('[fantia-downloader] download files not found.');
        }
        observer.disconnect();
      }
    });
    const hasMainThumbnail = findPostMainThumb();
    if (hasMainThumbnail) {
      createDownloadButton();
    } else {
      observer.observe(mainElm, {
        childList: true,
        subtree: true,
      });
    }
  };
  observeMainBlock();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址