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.

  1. // ==UserScript==
  2. // @name 4chan Archive Image Downloader
  3. // @namespace Violentmonkey Scripts
  4. // @match https://archive.4plebs.org/*/thread/*
  5. // @match https://desuarchive.org/*/thread/*
  6. // @match https://boards.fireden.net/*/thread/*
  7. // @match https://archived.moe/*/thread/*
  8. // @match https://thebarchive.com/*/thread/*
  9. // @match https://archiveofsins.com/*/thread/*
  10. // @match https://archive.alice.al/*/thread/*
  11. // @match https://arch.b4k.co/*/thread/*
  12. // @match https://archive.palanq.win/*/thread/*
  13. // @grant GM_download
  14. // @grant GM_registerMenuCommand
  15. // @version 1.4.2
  16. // @license The Unlicense
  17. // @author ImpatientImport
  18. // @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.
  19. // ==/UserScript==
  20.  
  21. /* EDIT BELOW THIS LINE */
  22.  
  23. // User preferences
  24. var indiv_button_enabled = true;
  25.  
  26. var api_button_enabled = false;
  27.  
  28. var keep_original_filenames = true; // Prioritized above archive_filenames
  29.  
  30. 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.
  31.  
  32. var confirm_download = true;
  33.  
  34. var download_limit = 3000; // speed in milliseconds to delay
  35.  
  36. var named_poster_media_download_only = false;
  37.  
  38. var named_poster_tag_in_filename = false; // Only used if named_poster_media_download_only is true
  39.  
  40. /* EDIT ABOVE THIS LINE */
  41.  
  42.  
  43. (function() {
  44. 'use strict';
  45.  
  46. // Constants for later reference
  47. const top_of_thread = document.getElementsByClassName("post_controls")[0];
  48. const thread_URL = document.URL;
  49. const archive_site = thread_URL.toString().split('/')[2];
  50. const url_path = new URL(thread_URL).pathname;
  51. const url_path_split = url_path.toString().split('/')
  52. const thread_board = url_path_split[1];
  53. const thread_num = url_path_split[3];
  54.  
  55.  
  56. // checking URL console
  57. /*
  58. console.log(url_path_split);
  59. console.log(url_path);
  60. console.log(thread_URL);
  61. console.log(thread_URL.toString().split('/')[2]);
  62. */
  63.  
  64. const api_url = "https://" + archive_site + "/_/api/chan/thread/?board=" + thread_board + "&num=" + thread_num; // important
  65. //console.log(api_url)
  66.  
  67. // Individual thread image downloader button
  68.  
  69. var indiv_dl_btn;
  70. var indiv_dlbtn_elem;
  71. var indivOriginalStyle;
  72. var indivOrigStyles;
  73. if (indiv_button_enabled){
  74. indiv_dl_btn = document.createElement('a');
  75. indiv_dl_btn.id = "indiv_btn";
  76. indiv_dl_btn.classList.add("btnr", "parent");
  77. indiv_dl_btn.innerText = "Indiv DL";
  78. top_of_thread.append(indiv_dl_btn);
  79.  
  80. indiv_dlbtn_elem = document.getElementById("indiv_btn");
  81. indivOriginalStyle = window.getComputedStyle(indiv_dl_btn);
  82.  
  83. indivOrigStyles = {
  84. backgroundColor: indivOriginalStyle.backgroundColor,
  85. color: indivOriginalStyle.color,
  86. }
  87. }
  88.  
  89.  
  90. // API button for getting the JSON of a thread in a new tab
  91.  
  92. var api_btn;
  93. var api_btn_elem;
  94. if (api_button_enabled){
  95. api_btn = document.createElement('a');
  96. api_btn.id = "api_btn";
  97. api_btn.href = api_url;
  98. api_btn.target = "new";
  99. api_btn.classList.add("btnr", "parent");
  100. api_btn.innerText = "Thread API";
  101. top_of_thread.append(api_btn);
  102.  
  103. api_btn_elem = document.getElementById("api_btn");
  104. }
  105.  
  106.  
  107. function displayButton (elem){
  108. console.log(elem);
  109.  
  110. var current_style = window.getComputedStyle(elem).backgroundColor;
  111. //console.log(current_style); // debug
  112.  
  113. var next_style;
  114.  
  115. const button_original_text = {"indiv_btn": "Indiv DL"};
  116.  
  117. const button_original_styles = {"indiv_btn": indivOrigStyles};
  118.  
  119. const confirmStyles = {
  120. backgroundColor: 'rgb(255, 64, 64)', // Coral Red
  121. color:"white",
  122. }
  123.  
  124. const processingStyles = {
  125. backgroundColor: 'rgb(238, 210, 2)', // Safety Yellow
  126. color:"black",
  127. }
  128.  
  129. const doneStyles = {
  130. backgroundColor: 'rgb(46, 139, 87)', // Sea Green
  131. color:"white",
  132.  
  133. }
  134.  
  135. const originalStyles = {
  136. backgroundColor: button_original_styles[elem.id].backgroundColor, // Original, clear
  137. color: button_original_styles[elem.id].color,
  138.  
  139. }
  140.  
  141. // Button style switcher
  142. switch (current_style) {
  143. case 'rgba(0, 0, 0, 0)': // Original color
  144. next_style = confirmStyles;
  145. elem.innerText = "Confirm?";
  146. break;
  147.  
  148. case 'rgb(255, 64, 64)': // Confirm color
  149. next_style = processingStyles;
  150. elem.innerText = "Processing";
  151. break;
  152.  
  153. case 'rgb(238, 210, 2)': // Processing color
  154. next_style = doneStyles;
  155. elem.innerText = "Done";
  156. break;
  157.  
  158. case 'rgb(46, 139, 87)': // Done Color
  159. next_style = originalStyles;
  160. elem.innerText = button_original_text[elem.id];
  161. break;
  162.  
  163. }
  164.  
  165. Object.assign(elem.style, next_style);
  166. }
  167.  
  168.  
  169. // Retrieves media from the thread (in JSON format)
  170. // If OP only, ignore posts, else get posts
  171. function retrieve_media(thread_obj) {
  172. var media_arr = [];
  173. var media_fnames = [];
  174. var return_value = [];
  175.  
  176. const OP = thread_obj[thread_num].op.media;
  177. //console.log(OP); // debug
  178.  
  179. //If OP is a massive OP,
  180. var OP_filename = (keep_original_filenames) ? OP.media_filename : OP.media_orig;
  181. OP_filename = (!keep_original_filenames && archive_filenames) ? OP.media : OP_filename;
  182. OP_filename = (named_poster_tag_in_filename && named_poster_media_download_only) ? String(thread_obj[thread_num].op.name+"_-_"+OP_filename) : OP_filename;
  183. var OP_media_link = (OP.media_link == null) ? OP.remote_media_link : OP.media_link;
  184. if (!named_poster_media_download_only || named_poster_media_download_only && thread_obj[thread_num].op.name != "Anonymous") {
  185. media_arr.push(OP_media_link);
  186. media_fnames.push(OP_filename);
  187. }
  188.  
  189. // Boolean, checks if posts are present in thread
  190. const posts_exist = thread_obj[thread_num].posts != undefined;
  191.  
  192. if (posts_exist) {
  193. const thread_posts = thread_obj[thread_num].posts;
  194. const post_nums = Object.keys(thread_posts);
  195. const posts_length = post_nums.length;
  196.  
  197. //Adds all post image urls and original filenames to the above arrays
  198. for (var i = 0; i < posts_length; i++) {
  199.  
  200. //equivalent to: thread[posts][post_num][media]
  201. var temp_media_post = thread_posts[post_nums[i]].media;
  202.  
  203. //if media exists (and is from a named poster [non-anonymous poster] if option true),
  204. //then push media to arrays
  205. if (temp_media_post !== null && (!named_poster_media_download_only || named_poster_media_download_only && thread_posts[post_nums[i]].name != "Anonymous") ) {
  206.  
  207. var working_media_link = (temp_media_post.media_link == null) ? temp_media_post.remote_media_link : temp_media_post.media_link
  208. var correct_media_fname = (keep_original_filenames) ? temp_media_post.media_filename : temp_media_post.media_orig;
  209. correct_media_fname = (!keep_original_filenames && archive_filenames) ? temp_media_post.media : correct_media_fname;
  210. 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;
  211.  
  212.  
  213. //console.log(working_media_link); //debug
  214. //console.log(correct_media_fname); //debug
  215.  
  216. media_arr.push(working_media_link)
  217. media_fnames.push(correct_media_fname);
  218.  
  219. }
  220. }
  221. }
  222.  
  223. // Adds the media link array with the media filenames array into the final return
  224. return_value[0] = media_arr;
  225. return_value[1] = media_fnames;
  226.  
  227. /*
  228. var count;
  229.  
  230. function download_with_scope(){
  231. GM_download(media_arr[count], media_fnames[count]) // downloads images
  232. }
  233. */
  234.  
  235. function sleep(ms) {
  236. return new Promise(resolve => setTimeout(resolve, ms));
  237. }
  238.  
  239. async function download_images(){
  240.  
  241. for (var i=0; i<media_arr.length; i++){
  242. //console.log(media_fnames[i] + " "+ media_arr[i]); //debug
  243. //console.log(download_limit); //debug
  244.  
  245. await sleep(download_limit);
  246.  
  247. GM_download(media_arr[i], media_fnames[i]) // downloads images
  248.  
  249. }
  250. }
  251.  
  252. download_images();
  253.  
  254.  
  255. if(confirm_download){
  256. displayButton(indiv_dlbtn_elem);
  257. setTimeout(displayButton(indiv_dlbtn_elem), 3000);
  258. }
  259.  
  260. }
  261.  
  262. // Gets the JSON file for the thread with the API
  263. async function get_archive_thread() {
  264. const API_response = await fetch(api_url);
  265. const JSON_file = await API_response.json();
  266. console.log(JSON_file); // debug
  267. retrieve_media(JSON_file);
  268. }
  269.  
  270. // Controls what the individual download button does upon being clicked
  271. function indivDownload(){
  272. displayButton(indiv_dlbtn_elem);
  273.  
  274. // Wait for user to confirm zip if didn't click fast enough for double-click
  275. setTimeout(function(){
  276. if (window.getComputedStyle(indiv_dl_btn).backgroundColor == 'rgb(255, 64, 64)'){
  277. indiv_dl_btn.removeEventListener("click", displayButton);
  278. indiv_dl_btn.addEventListener("click", get_archive_thread);
  279.  
  280. // If user does not confirm, reset the button back to original
  281. setTimeout(function(){
  282. indiv_dl_btn.removeEventListener("click", get_archive_thread);
  283. indiv_dl_btn.addEventListener("click", displayButton);
  284. Object.assign(indiv_dlbtn_elem.style, indivOrigStyles);
  285. indiv_dl_btn.innerText = "Indiv DL";
  286. }, 5000);
  287.  
  288. }
  289. }, 501);
  290.  
  291. }
  292.  
  293. GM_registerMenuCommand("Download all thread images individually", get_archive_thread);
  294.  
  295.  
  296. // Download thread button event listener(s)
  297. if(confirm_download){
  298. indiv_dlbtn_elem.addEventListener("click", indivDownload);
  299. indiv_dlbtn_elem.addEventListener("dblclick", get_archive_thread);
  300. }
  301. else{
  302. indiv_dlbtn_elem.addEventListener("click", get_archive_thread);
  303. }
  304.  
  305. })();