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-25 提交的版本,查看 最新版本

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