Desu Image Downloader

Download images with original filenames on desuarchive.org, archive.palanq.win, and add download button to direct image pages

当前为 2024-12-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Desu Image Downloader
  3. // @version 4.0
  4. // @description Download images with original filenames on desuarchive.org, archive.palanq.win, and add download button to direct image pages
  5. // @author Anonimas
  6. // @match https://desuarchive.org/*
  7. // @match https://desu-usergeneratedcontent.xyz/*
  8. // @match https://archive.palanq.win/*
  9. // @match https://archive-media.palanq.win/*
  10. // @grant GM_download
  11. // @grant GM_addStyle
  12. // @namespace https://greasyfork.org/users/1342214
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. GM_addStyle(`
  19. #filename-search-container {
  20. position: fixed !important;
  21. bottom: 20px !important;
  22. right: 20px !important;
  23. display: flex !important;
  24. align-items: center !important;
  25. background-color: rgba(0, 0, 0, 0.5) !important;
  26. border-radius: 8px !important;
  27. padding: 0 8px !important;
  28. transition: background-color 0.3s !important;
  29. z-index: 9998 !important;
  30. height: 44px !important;
  31. box-sizing: border-box !important;
  32. }
  33. #filename-search-container:hover {
  34. background-color: rgba(0, 0, 0, 0.7) !important;
  35. }
  36. #filename-search-input {
  37. background-color: transparent !important;
  38. border: none !important;
  39. color: white !important;
  40. font-size: 18px !important;
  41. padding: 0 12px !important;
  42. width: 250px !important;
  43. height: 100% !important;
  44. outline: none !important;
  45. font-family: Arial, sans-serif !important;
  46. line-height: 44px !important;
  47. margin: 0 !important;
  48. box-shadow: none !important;
  49. }
  50. #filename-search-input::placeholder {
  51. color: rgba(255, 255, 255, 0.7) !important;
  52. }
  53. #filename-search-input:focus {
  54. outline: none !important;
  55. box-shadow: none !important;
  56. border: none !important;
  57. background-color: transparent !important;
  58. }
  59. #filename-search-button {
  60. background-color: transparent !important;
  61. color: white !important;
  62. border: none !important;
  63. padding: 0 16px !important;
  64. height: 100% !important;
  65. cursor: pointer !important;
  66. font-size: 18px !important;
  67. font-family: Arial, sans-serif !important;
  68. transition: background-color 0.3s !important;
  69. line-height: 44px !important;
  70. margin: 0 !important;
  71. }
  72. #filename-search-button:hover {
  73. background-color: rgba(255, 255, 255, 0.1) !important;
  74. border-radius: 5px !important;
  75. }
  76. #download-button {
  77. position: fixed;
  78. bottom: 20px;
  79. right: 20px;
  80. background-color: rgba(0, 0, 0, 0.5);
  81. color: white;
  82. border: none;
  83. border-radius: 5px;
  84. padding: 10px 20px;
  85. cursor: pointer;
  86. font-size: 16px;
  87. transition: background-color 0.3s;
  88. text-decoration: none;
  89. font-family: Arial, sans-serif;
  90. z-index: 9999;
  91. display: none; /* Hidden by default */
  92. }
  93. #download-button:hover {
  94. background-color: rgba(0, 0, 0, 0.7);
  95. }
  96. body.has-download-button #filename-search-container {
  97. right: 140px !important;
  98. }
  99. `);
  100.  
  101. // Helper function to get full filename from an element
  102. function getFullFilename(element) {
  103. return element?.getAttribute('title') || element?.textContent?.trim() || null;
  104. }
  105.  
  106.  
  107. //Helper Function to extract filename from a URL.
  108. function extractFilenameFromUrl(url) {
  109. try {
  110. const parsedUrl = new URL(url);
  111. const pathname = parsedUrl.pathname;
  112. return pathname.substring(pathname.lastIndexOf('/') + 1);
  113. } catch (e) {
  114. console.error("Error parsing URL", url, e);
  115. return null;
  116. }
  117. }
  118.  
  119. //Helper function to append the filename to the url.
  120. function appendFilenameToUrl(url, filename) {
  121. try {
  122. const parsedUrl = new URL(url);
  123. parsedUrl.searchParams.set('filename', filename);
  124. return parsedUrl.toString();
  125. }
  126. catch(e) {
  127. console.error("Error modifying URL", url, e);
  128. return url;
  129. }
  130. }
  131.  
  132.  
  133. // Function to download a single image with GM_download
  134. function downloadImage(imageUrl, originalFilename) {
  135. if (!imageUrl || !originalFilename) {
  136. console.error("Invalid image URL or filename:", { imageUrl, originalFilename });
  137. return;
  138. }
  139.  
  140. GM_download({
  141. url: imageUrl,
  142. name: originalFilename,
  143. onload: () => {},
  144. onerror: (error) => console.error('Download error:', error)
  145. });
  146. }
  147.  
  148. // Function to handle image click (opening image in new tab with filename)
  149. function handleImageClick(event) {
  150. event.preventDefault(); // Prevent the default link behavior
  151.  
  152. const imageLink = event.target.closest('a[href*="//desu-usergeneratedcontent.xyz/"], a[href*="//archive-media.palanq.win/"]');
  153. if (!imageLink) return; // Exit if no image link is found
  154.  
  155. const imageUrl = imageLink.href;
  156. let filenameElement = imageLink.closest('div.post_file, article.thread, article.post')?.querySelector('a.post_file_filename');
  157. if (!filenameElement) return;
  158.  
  159. const originalFilename = getFullFilename(filenameElement);
  160. const newUrl = appendFilenameToUrl(imageUrl, originalFilename);
  161. window.open(newUrl, '_blank');
  162. }
  163.  
  164.  
  165. // Function to create the search interface
  166. function createSearchInterface() {
  167. const searchContainer = document.createElement('div');
  168. searchContainer.id = 'filename-search-container';
  169.  
  170. const searchInput = document.createElement('input');
  171. searchInput.id = 'filename-search-input';
  172. searchInput.type = 'text';
  173. searchInput.placeholder = 'Search filename...';
  174. searchInput.autocomplete = 'off';
  175.  
  176. const searchButton = document.createElement('button');
  177. searchButton.id = 'filename-search-button';
  178. searchButton.textContent = 'Search';
  179.  
  180. const performSearch = () => {
  181. const searchTerm = searchInput.value.trim();
  182. if (!searchTerm) return;
  183.  
  184. let searchUrl;
  185. const currentBoard = window.location.pathname.split('/')[1] || 'a';
  186. if (window.location.hostname === 'archive.palanq.win') {
  187. searchUrl = `https://archive.palanq.win/${currentBoard}/search/filename/${encodeURIComponent(searchTerm)}/`;
  188. } else {
  189. searchUrl = `https://desuarchive.org/${currentBoard}/search/filename/${encodeURIComponent(searchTerm)}/`;
  190. }
  191. window.location.href = searchUrl;
  192. };
  193.  
  194. searchButton.addEventListener('click', performSearch);
  195. searchInput.addEventListener('keypress', (e) => {
  196. if (e.key === 'Enter') {
  197. performSearch();
  198. }
  199. });
  200.  
  201.  
  202. searchContainer.appendChild(searchInput);
  203. searchContainer.appendChild(searchButton);
  204. return searchContainer;
  205. }
  206.  
  207. // Function to add the download button to direct image pages
  208. function addDownloadButtonToImagePage() {
  209. if (!(window.location.hostname === 'desu-usergeneratedcontent.xyz' || window.location.hostname === 'archive-media.palanq.win')) {
  210. return; // Exit if not on an image page
  211. }
  212.  
  213.  
  214. if (document.getElementById('download-button')) {
  215. return;
  216. }
  217.  
  218. const button = document.createElement('a');
  219. button.id = 'download-button';
  220. button.textContent = 'Download';
  221.  
  222.  
  223. const imageUrl = window.location.href.split('?')[0];
  224. button.href = imageUrl;
  225.  
  226. const urlParams = new URLSearchParams(window.location.search);
  227. const originalFilename = urlParams.get('filename') || extractFilenameFromUrl(imageUrl);
  228.  
  229.  
  230. button.download = originalFilename;
  231. document.body.classList.add('has-download-button');
  232. document.body.appendChild(button);
  233.  
  234. button.addEventListener('click', event => {
  235. event.preventDefault();
  236. downloadImage(imageUrl, originalFilename);
  237. });
  238.  
  239. //Make download button visable
  240. button.style.display = 'block';
  241. }
  242.  
  243.  
  244. // Event delegation for image downloads and filename handling
  245. function setupEventDelegation() {
  246. document.body.addEventListener('click', function(event) {
  247. const target = event.target;
  248.  
  249. //Direct Download from File Name
  250. if(target.closest('a.post_file_filename')) {
  251. event.preventDefault();
  252. const link = target.closest('a.post_file_filename');
  253. if (!link) return;
  254.  
  255. const imageUrl = link.href;
  256. const originalFilename = getFullFilename(link);
  257. downloadImage(imageUrl,originalFilename);
  258. return;
  259. }
  260. //Direct Download from Icon
  261. if (target.closest('a[href*="//desu-usergeneratedcontent.xyz/"] i.icon-download-alt, a[href*="//archive-media.palanq.win/"] i.icon-download-alt')) {
  262. event.preventDefault();
  263. const downloadButton = target.closest('a');
  264. if (!downloadButton) return;
  265.  
  266. const imageUrl = downloadButton.href;
  267. let filenameElement = downloadButton.closest('div.post_file, article.thread, article.post')?.querySelector('a.post_file_filename');
  268. if (!filenameElement) return;
  269.  
  270. const originalFilename = getFullFilename(filenameElement);
  271. downloadImage(imageUrl,originalFilename);
  272. return;
  273.  
  274. }
  275.  
  276. //Handle image click
  277. if (target.closest('a[href*="//desu-usergeneratedcontent.xyz/"] img, a[href*="//archive-media.palanq.win/"] img')) {
  278. handleImageClick(event);
  279. }
  280. });
  281. }
  282.  
  283. // Initialize
  284. function initialize() {
  285. if (window.location.hostname === 'desuarchive.org' || window.location.hostname === 'archive.palanq.win') {
  286. if (!document.getElementById('filename-search-container')) {
  287. const searchContainer = createSearchInterface();
  288. document.body.appendChild(searchContainer);
  289. }
  290. setupEventDelegation();
  291. }
  292.  
  293. addDownloadButtonToImagePage();
  294.  
  295. // Setup observer for dynamic content
  296. const observer = new MutationObserver(debounce(handleMutations, 200));
  297. observer.observe(document.body, { childList: true, subtree: true });
  298. }
  299.  
  300. // Mutation Handling
  301. function handleMutations(mutations) {
  302. for (const mutation of mutations) {
  303. if (mutation.addedNodes.length) {
  304. const newLinks = document.querySelectorAll('a.post_file_filename:not([data-handled])');
  305. newLinks.forEach(link => {
  306. link.dataset.handled = 'true';
  307. });
  308. }
  309. }
  310. }
  311.  
  312.  
  313. //Debounce Function
  314. function debounce(func, delay) {
  315. let timeout;
  316. return function(...args) {
  317. const context = this;
  318. clearTimeout(timeout);
  319. timeout = setTimeout(() => func.apply(context, args), delay);
  320. };
  321. }
  322.  
  323.  
  324. initialize();
  325. })();