fantia-image-downloader

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

اعتبارا من 31-01-2023. شاهد أحدث إصدار.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name               fantia-image-downloader
// @name:en            fantia-image-downloader
// @name:zh-CN         fantia-image-downloader
// @namespace          https://fantia.jp/
// @version            0.1.2
// @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 getDLButton = () => document.querySelector('#fantiaDLbutton');
  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');
    const [date = false] = postDate?.textContent?.split(' ').filter(Boolean) || [];
    if (!date) {
      return getNowDate();
    }
    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 = getDLButton();
        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 url = new URL(originalImage.src);
              const ext = url.pathname.split('.').pop() || 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 = getDLButton();
          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: 30px;
  }
  .userjs-downloadBtn__button {
    position: relative;
    appearance: none;
    margin: 0;
    padding: 12px 24px;
    font-size: 16px;
    color: #fff;
    background-color: #00d1b2;
    border: 0;
    border-radius: 4px;
    box-shadow: 0 3px 6px rgb(0 209 178 / 60%);
  }
  .userjs-downloadBtn__button:active {
    top: 1px;
    box-shadow: none;
  }
  .userjs-downloadBtn__button.is-disabled {
    pointer-events: none;
    user-select: none;
    color: #aaa;
    background-color: #e0e0e0;
    box-shadow: none;
  }
  </style>`;
  const createDownloadButton = () => {
    const postHeader = document.querySelector('.the-post .post-header');
    const buttonHTML = `<div class="userjs-downloadBtn">
      <button type="button" id="fantiaDLbutton" class="userjs-downloadBtn__button">${lang['initialButtonName']}</button>
    </div>`;
    document.head.insertAdjacentHTML('afterbegin', buttonStyle);
    postHeader?.insertAdjacentHTML('afterend', buttonHTML);
    const buttonElm = getDLButton();
    buttonElm?.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().filter((data) => data.imagePaths.length);
        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();
})();