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.

目前為 2024-01-23 提交的版本,檢視 最新版本

  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://boards.fireden.net/*/thread/*
  8. // @match https://archived.moe/*/thread/*
  9. // @match https://thebarchive.com/*/thread/*
  10. // @match https://archiveofsins.com/*/thread/*
  11. // @match https://www.tokyochronos.net/*/thread/*
  12. // @match https://archive.wakarimasen.moe/*/thread/*
  13. // @match https://archive.alice.al/*/thread/*
  14. // @grant GM_download
  15. // @grant GM_registerMenuCommand
  16. // @version 1.3.1
  17. // @license The Unlicense
  18. // @author ImpatientImport
  19. // @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.
  20. // ==/UserScript==
  21.  
  22. /* EDIT BELOW THIS LINE */
  23.  
  24. // User preferences
  25. var indiv_button_enabled = true;
  26.  
  27. var api_button_enabled = false;
  28.  
  29. var keep_original_filenames = true;
  30.  
  31. var confirm_download = true;
  32.  
  33. var download_limit = 3000; // speed in milliseconds to delay
  34.  
  35. var named_poster_media_download_only = false;
  36.  
  37. /* EDIT ABOVE THIS LINE */
  38.  
  39.  
  40. (function() {
  41. 'use strict';
  42.  
  43. // Constants for later reference
  44. const top_of_thread = document.getElementsByClassName("post_controls")[0];
  45. const thread_URL = document.URL;
  46. const archive_site = thread_URL.toString().split('/')[2];
  47. const url_path = new URL(thread_URL).pathname;
  48. const url_path_split = url_path.toString().split('/')
  49. const thread_board = url_path_split[1];
  50. const thread_num = url_path_split[3];
  51.  
  52.  
  53. // checking URL console
  54. /*
  55. console.log(url_path_split);
  56. console.log(url_path);
  57. console.log(thread_URL);
  58. console.log(thread_URL.toString().split('/')[2]);
  59. */
  60.  
  61. const api_url = "https://" + archive_site + "/_/api/chan/thread/?board=" + thread_board + "&num=" + thread_num; // important
  62. //console.log(api_url)
  63.  
  64. // Individual thread image downloader button
  65.  
  66. var indiv_dl_btn;
  67. var indiv_dlbtn_elem;
  68. var indivOriginalStyle;
  69. var indivOrigStyles;
  70. if (indiv_button_enabled){
  71. indiv_dl_btn = document.createElement('a');
  72. indiv_dl_btn.id = "indiv_btn";
  73. indiv_dl_btn.classList.add("btnr", "parent");
  74. indiv_dl_btn.innerText = "Indiv DL";
  75. top_of_thread.append(indiv_dl_btn);
  76.  
  77. indiv_dlbtn_elem = document.getElementById("indiv_btn");
  78. indivOriginalStyle = window.getComputedStyle(indiv_dl_btn);
  79.  
  80. indivOrigStyles = {
  81. backgroundColor: indivOriginalStyle.backgroundColor,
  82. color: indivOriginalStyle.color,
  83. }
  84. }
  85.  
  86.  
  87. // API button for getting the JSON of a thread in a new tab
  88.  
  89. var api_btn;
  90. var api_btn_elem;
  91. if (api_button_enabled){
  92. api_btn = document.createElement('a');
  93. api_btn.id = "api_btn";
  94. api_btn.href = api_url;
  95. api_btn.target = "new";
  96. api_btn.classList.add("btnr", "parent");
  97. api_btn.innerText = "Thread API";
  98. top_of_thread.append(api_btn);
  99.  
  100. api_btn_elem = document.getElementById("api_btn");
  101. }
  102.  
  103.  
  104. function displayButton (elem){
  105. console.log(elem);
  106.  
  107. var current_style = window.getComputedStyle(elem).backgroundColor;
  108. //console.log(current_style); // debug
  109.  
  110. var next_style;
  111.  
  112. const button_original_text = {"indiv_btn": "Indiv DL"};
  113.  
  114. const button_original_styles = {"indiv_btn": indivOrigStyles};
  115.  
  116. const confirmStyles = {
  117. backgroundColor: 'rgb(255, 64, 64)', // Coral Red
  118. color:"white",
  119. }
  120.  
  121. const processingStyles = {
  122. backgroundColor: 'rgb(238, 210, 2)', // Safety Yellow
  123. color:"black",
  124. }
  125.  
  126. const doneStyles = {
  127. backgroundColor: 'rgb(46, 139, 87)', // Sea Green
  128. color:"white",
  129.  
  130. }
  131.  
  132. const originalStyles = {
  133. backgroundColor: button_original_styles[elem.id].backgroundColor, // Original, clear
  134. color: button_original_styles[elem.id].color,
  135.  
  136. }
  137.  
  138. // Button style switcher
  139. switch (current_style) {
  140. case 'rgba(0, 0, 0, 0)': // Original color
  141. next_style = confirmStyles;
  142. elem.innerText = "Confirm?";
  143. break;
  144.  
  145. case 'rgb(255, 64, 64)': // Confirm color
  146. next_style = processingStyles;
  147. elem.innerText = "Processing";
  148. break;
  149.  
  150. case 'rgb(238, 210, 2)': // Processing color
  151. next_style = doneStyles;
  152. elem.innerText = "Done";
  153. break;
  154.  
  155. case 'rgb(46, 139, 87)': // Done Color
  156. next_style = originalStyles;
  157. elem.innerText = button_original_text[elem.id];
  158. break;
  159.  
  160. }
  161.  
  162. Object.assign(elem.style, next_style);
  163. }
  164.  
  165.  
  166. // Retrieves media from the thread (in JSON format)
  167. // If OP only, ignore posts, else get posts
  168. function retrieve_media(thread_obj) {
  169. var media_arr = [];
  170. var media_fnames = [];
  171. var return_value = [];
  172.  
  173. const OP = thread_obj[thread_num].op.media;
  174. //console.log(OP); // debug
  175.  
  176. //If OP is a massive OP,
  177. if (named_poster_media_download_only && thread_obj[thread_num].op.name != "Anonymous") {
  178. media_arr.push(OP.remote_media_link);
  179. media_fnames.push(OP.media_filename);
  180. }
  181. else if (!named_poster_media_download_only) {
  182. media_arr.push(OP.remote_media_link);
  183. media_fnames.push(OP.media_filename);
  184. }
  185.  
  186. // Boolean, checks if posts are present in thread
  187. const posts_exist = thread_obj[thread_num].posts != undefined;
  188.  
  189. if (posts_exist) {
  190. const thread_posts = thread_obj[thread_num].posts;
  191. const post_nums = Object.keys(thread_posts);
  192. const posts_length = post_nums.length;
  193.  
  194. //Adds all post image urls and original filenames to the above arrays
  195. for (var i = 0; i < posts_length; i++) {
  196.  
  197. //equivalent to: thread[posts][post_num][media]
  198. var temp_media_post = thread_posts[post_nums[i]].media;
  199.  
  200. //if media exists (and is from a named poster [non-anonymous poster] if option true),
  201. //then push media to arrays
  202.  
  203. //console.log(temp_media_post.remote_media_link)
  204.  
  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. media_arr.push(temp_media_post.remote_media_link)
  208.  
  209. if (keep_original_filenames){
  210. media_fnames.push(temp_media_post.media_filename);
  211.  
  212. //console.log(temp_media_post.media_filename); //debug
  213. }
  214. else{
  215. media_fnames.push(temp_media_post.media_orig);
  216.  
  217. //console.log(temp_media_post.media_orig); //debug
  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. })();