Greasy Fork is available in English.

4chan Archive Image Downloader

4chan archive thread image downloader for general use across many foolfuuka based imageboards. Downloads all images individually in a thread with original filenames (by default). Optional thread API button, for development purposes.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        4chan Archive Image Downloader
// @namespace   Violentmonkey Scripts
// @match       https://archive.4plebs.org/*/thread/*
// @match       https://desuarchive.org/*/thread/*
// @match       https://boards.fireden.net/*/thread/*
// @match       https://archived.moe/*/thread/*
// @match       https://thebarchive.com/*/thread/*
// @match       https://archiveofsins.com/*/thread/*
// @match       https://archive.alice.al/*/thread/*
// @match       https://arch.b4k.co/*/thread/*
// @match       https://archive.palanq.win/*/thread/*
// @grant       GM_download
// @grant       GM_registerMenuCommand
// @version     1.4.2
// @license     The Unlicense
// @author      ImpatientImport
// @description 4chan archive thread image downloader for general use across many foolfuuka based imageboards. Downloads all images individually in a thread with original filenames (by default). Optional thread API button, for development purposes.
// ==/UserScript==

/* EDIT BELOW THIS LINE */

// User preferences
var indiv_button_enabled = true;

var api_button_enabled = false;

var keep_original_filenames = true; // Prioritized above archive_filenames

var archive_filenames = true; // Only used if keep_original_filenames is false. If so, archive names will be saved; otherwise, 4chan names will be saved.

var confirm_download = true;

var download_limit = 3000; // speed in milliseconds to delay

var named_poster_media_download_only = false;

var named_poster_tag_in_filename = false; // Only used if named_poster_media_download_only is true

/* EDIT ABOVE THIS LINE */


(function() {
  'use strict';

  // Constants for later reference
  const top_of_thread = document.getElementsByClassName("post_controls")[0];
  const thread_URL = document.URL;
  const archive_site = thread_URL.toString().split('/')[2];
  const url_path = new URL(thread_URL).pathname;
  const url_path_split = url_path.toString().split('/')
  const thread_board = url_path_split[1];
  const thread_num = url_path_split[3];


  // checking URL console
  /*
  console.log(url_path_split);
  console.log(url_path);
  console.log(thread_URL);
  console.log(thread_URL.toString().split('/')[2]);
  */

  const api_url = "https://" + archive_site + "/_/api/chan/thread/?board=" + thread_board + "&num=" + thread_num; // important
  //console.log(api_url)

  // Individual thread image downloader button

  var indiv_dl_btn;
  var indiv_dlbtn_elem;
  var indivOriginalStyle;
  var indivOrigStyles;
  if (indiv_button_enabled){
    indiv_dl_btn = document.createElement('a');
    indiv_dl_btn.id = "indiv_btn";
    indiv_dl_btn.classList.add("btnr", "parent");
    indiv_dl_btn.innerText = "Indiv DL";
    top_of_thread.append(indiv_dl_btn);

    indiv_dlbtn_elem = document.getElementById("indiv_btn");
    indivOriginalStyle = window.getComputedStyle(indiv_dl_btn);

    indivOrigStyles = {
      backgroundColor: indivOriginalStyle.backgroundColor,
      color: indivOriginalStyle.color,
    }
  }


  // API button for getting the JSON of a thread in a new tab

  var api_btn;
  var api_btn_elem;
  if (api_button_enabled){
    api_btn = document.createElement('a');
    api_btn.id = "api_btn";
    api_btn.href = api_url;
    api_btn.target = "new";
    api_btn.classList.add("btnr", "parent");
    api_btn.innerText = "Thread API";
    top_of_thread.append(api_btn);

    api_btn_elem = document.getElementById("api_btn");
  }


  function displayButton (elem){
    console.log(elem);

    var current_style = window.getComputedStyle(elem).backgroundColor;
    //console.log(current_style); // debug

    var next_style;

    const button_original_text = {"indiv_btn": "Indiv DL"};

    const button_original_styles = {"indiv_btn": indivOrigStyles};

    const confirmStyles = {
      backgroundColor: 'rgb(255, 64, 64)', // Coral Red
      color:"white",
    }

    const processingStyles = {
      backgroundColor: 'rgb(238, 210, 2)', // Safety Yellow
      color:"black",
    }

    const doneStyles = {
      backgroundColor: 'rgb(46, 139, 87)', // Sea Green
      color:"white",

    }

    const originalStyles = {
      backgroundColor: button_original_styles[elem.id].backgroundColor, // Original, clear
      color: button_original_styles[elem.id].color,

    }

    // Button style switcher
    switch (current_style) {
      case 'rgba(0, 0, 0, 0)': // Original color
        next_style = confirmStyles;
        elem.innerText = "Confirm?";
        break;

      case 'rgb(255, 64, 64)': // Confirm color
        next_style = processingStyles;
        elem.innerText = "Processing";
        break;

      case 'rgb(238, 210, 2)': // Processing color
        next_style = doneStyles;
        elem.innerText = "Done";
        break;

      case 'rgb(46, 139, 87)': // Done Color
        next_style = originalStyles;
        elem.innerText = button_original_text[elem.id];
        break;

    }

    Object.assign(elem.style, next_style);
  }


  // Retrieves media from the thread (in JSON format)
  // If OP only, ignore posts, else get posts
  function retrieve_media(thread_obj) {
    var media_arr = [];
    var media_fnames = [];
    var return_value = [];

    const OP = thread_obj[thread_num].op.media;
    //console.log(OP); // debug

    //If OP is a massive OP,
    var OP_filename = (keep_original_filenames) ? OP.media_filename : OP.media_orig;
    OP_filename = (!keep_original_filenames && archive_filenames) ? OP.media : OP_filename;
    OP_filename = (named_poster_tag_in_filename && named_poster_media_download_only) ? String(thread_obj[thread_num].op.name+"_-_"+OP_filename) : OP_filename;
    var OP_media_link = (OP.media_link == null) ? OP.remote_media_link : OP.media_link;
    if (!named_poster_media_download_only || named_poster_media_download_only && thread_obj[thread_num].op.name != "Anonymous") {
      media_arr.push(OP_media_link);
      media_fnames.push(OP_filename);
    }

    // Boolean, checks if posts are present in thread
    const posts_exist = thread_obj[thread_num].posts != undefined;

    if (posts_exist) {
      const thread_posts = thread_obj[thread_num].posts;
      const post_nums = Object.keys(thread_posts);
      const posts_length = post_nums.length;

      //Adds all post image urls and original filenames to the above arrays
      for (var i = 0; i < posts_length; i++) {

        //equivalent to: thread[posts][post_num][media]
        var temp_media_post = thread_posts[post_nums[i]].media;

        //if media exists (and is from a named poster [non-anonymous poster] if option true),
        //then push media to arrays
        if (temp_media_post !== null && (!named_poster_media_download_only || named_poster_media_download_only && thread_posts[post_nums[i]].name != "Anonymous") ) {

          var working_media_link = (temp_media_post.media_link == null) ? temp_media_post.remote_media_link : temp_media_post.media_link
          var correct_media_fname = (keep_original_filenames) ? temp_media_post.media_filename : temp_media_post.media_orig;
          correct_media_fname = (!keep_original_filenames && archive_filenames) ? temp_media_post.media : correct_media_fname;
          correct_media_fname = (named_poster_tag_in_filename && named_poster_media_download_only) ? String(thread_posts[post_nums[i]].name+"_-_"+correct_media_fname) : correct_media_fname;


          //console.log(working_media_link); //debug
          //console.log(correct_media_fname); //debug

          media_arr.push(working_media_link)
          media_fnames.push(correct_media_fname);

        }
      }
    }

    // Adds the media link array with the media filenames array into the final return
    return_value[0] = media_arr;
    return_value[1] = media_fnames;

    /*
    var count;

    function download_with_scope(){
      GM_download(media_arr[count], media_fnames[count]) // downloads images
    }
    */

    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
   }

    async function download_images(){

      for (var i=0; i<media_arr.length; i++){
        //console.log(media_fnames[i] + " "+ media_arr[i]); //debug
        //console.log(download_limit); //debug

        await sleep(download_limit);

        GM_download(media_arr[i], media_fnames[i]) // downloads images

      }
    }

    download_images();


    if(confirm_download){
      displayButton(indiv_dlbtn_elem);
      setTimeout(displayButton(indiv_dlbtn_elem), 3000);
    }

  }

  // Gets the JSON file for the thread with the API
  async function get_archive_thread() {
    const API_response = await fetch(api_url);
    const JSON_file = await API_response.json();
    console.log(JSON_file); // debug
    retrieve_media(JSON_file);
  }

    // Controls what the individual download button does upon being clicked
  function indivDownload(){
    displayButton(indiv_dlbtn_elem);

    // Wait for user to confirm zip if didn't click fast enough for double-click
    setTimeout(function(){
      if (window.getComputedStyle(indiv_dl_btn).backgroundColor == 'rgb(255, 64, 64)'){
        indiv_dl_btn.removeEventListener("click", displayButton);
        indiv_dl_btn.addEventListener("click", get_archive_thread);

        // If user does not confirm, reset the button back to original
        setTimeout(function(){
          indiv_dl_btn.removeEventListener("click", get_archive_thread);
          indiv_dl_btn.addEventListener("click", displayButton);
          Object.assign(indiv_dlbtn_elem.style, indivOrigStyles);
          indiv_dl_btn.innerText = "Indiv DL";
        }, 5000);

      }
    }, 501);

  }

  GM_registerMenuCommand("Download all thread images individually", get_archive_thread);


  // Download thread button event listener(s)
  if(confirm_download){
    indiv_dlbtn_elem.addEventListener("click", indivDownload);
    indiv_dlbtn_elem.addEventListener("dblclick", get_archive_thread);
  }
  else{
    indiv_dlbtn_elem.addEventListener("click", get_archive_thread);
  }

})();