// ==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://boards.fireden.net/*/thread/*
// @match https://archived.moe/*/thread/*
// @match https://thebarchive.com/*/thread/*
// @match https://archiveofsins.com/*/thread/*
// @match https://www.tokyochronos.net/*/thread/*
// @match https://archive.wakarimasen.moe/*/thread/*
// @match https://archive.alice.al/*/thread/*
// @grant GM_download
// @grant GM_registerMenuCommand
// @version 1.1
// @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==
(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)
/* EDIT ABOVE THIS LINE */
// User preferences
var indiv_button_enabled = true;
var api_button_enabled = false;
var keep_original_filenames = true;
var confirm_download = true;
/* EDIT ABOVE THIS LINE */
// 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
media_arr.push(OP.media_link);
media_fnames.push(OP.media_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 (let 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,
if (temp_media_post !== null) {
//then push media to arrays
media_arr.push(temp_media_post.media_link)
if (keep_original_filenames){
media_fnames.push(temp_media_post.media_filename);
//console.log(temp_media_post.media_filename); //debug
}
else{
media_fnames.push(temp_media_post.media_orig);
//console.log(temp_media_post.media_orig); //debug
}
}
}
}
// Adds the media link array with the media filenames array into the final return
return_value[0] = media_arr;
return_value[1] = media_fnames;
for (var i=0; i<media_arr.length; i++){
//console.log(media_fnames[i] + " "+ media_arr[i]); //debug
GM_download(media_arr[i], media_fnames[i]);
}
displayButton(indiv_dlbtn_elem);
setTimeout(displayButton(indiv_dlbtn_elem), 3000);
}
// Gets the JSON file for the 4plebs 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);
}
})();