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 3.1
  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. }
  92. #download-button:hover {
  93. background-color: rgba(0, 0, 0, 0.7);
  94. }
  95. body.has-download-button #filename-search-container {
  96. right: 140px !important;
  97. }
  98. `);
  99.  
  100. function getFullFilename(element) {
  101. return element.getAttribute('title') || element.textContent.trim();
  102. }
  103.  
  104. function appendFilenameToUrl(url, filename) {
  105. return `${url}?filename=${encodeURIComponent(filename)}`;
  106. }
  107.  
  108. function extractFilenameFromUrl(url) {
  109. return url.substring(url.lastIndexOf('/') + 1);
  110. }
  111.  
  112. function downloadImage(imageUrl, originalFilename) {
  113. if (imageUrl && originalFilename) {
  114. GM_download({
  115. url: imageUrl,
  116. name: originalFilename,
  117. onload: () => {},
  118. onerror: (error) => console.error('Download error:', error)
  119. });
  120. }
  121. }
  122.  
  123. function handleImageClick(event) {
  124. event.preventDefault();
  125. const imageLink = event.target.closest('a[href*="//desu-usergeneratedcontent.xyz/"], a[href*="//archive-media.palanq.win/"]');
  126. if (!imageLink) return;
  127.  
  128. const imageUrl = imageLink.href;
  129. let filenameElement = imageLink.closest('div.post_file, article.thread, article.post')?.querySelector('a.post_file_filename');
  130. if (!filenameElement) return;
  131.  
  132. const originalFilename = getFullFilename(filenameElement);
  133. const newUrl = appendFilenameToUrl(imageUrl, originalFilename);
  134. window.open(newUrl, '_blank');
  135. }
  136.  
  137. function createSearchInterface() {
  138. const searchContainer = document.createElement('div');
  139. searchContainer.id = 'filename-search-container';
  140.  
  141. const searchInput = document.createElement('input');
  142. searchInput.id = 'filename-search-input';
  143. searchInput.type = 'text';
  144. searchInput.placeholder = 'Search filename...';
  145. searchInput.autocomplete = 'off';
  146.  
  147. const searchButton = document.createElement('button');
  148. searchButton.id = 'filename-search-button';
  149. searchButton.textContent = 'Search';
  150.  
  151. const performSearch = () => {
  152. const searchTerm = searchInput.value.trim();
  153. if (searchTerm) {
  154. let searchUrl;
  155. const currentBoard = window.location.pathname.split('/')[1] || 'a';
  156. if (window.location.hostname === 'archive.palanq.win') {
  157. searchUrl = `https://archive.palanq.win/${currentBoard}/search/filename/${encodeURIComponent(searchTerm)}/`;
  158. } else {
  159. searchUrl = `https://desuarchive.org/${currentBoard}/search/filename/${encodeURIComponent(searchTerm)}/`;
  160. }
  161. window.location.href = searchUrl;
  162. }
  163. };
  164.  
  165. searchButton.addEventListener('click', performSearch);
  166. searchInput.addEventListener('keypress', (e) => {
  167. if (e.key === 'Enter') {
  168. performSearch();
  169. }
  170. });
  171.  
  172. searchContainer.appendChild(searchInput);
  173. searchContainer.appendChild(searchButton);
  174. return searchContainer;
  175. }
  176.  
  177. function addDownloadButtonToImagePage() {
  178. if ((window.location.hostname === 'desu-usergeneratedcontent.xyz' ||
  179. window.location.hostname === 'archive-media.palanq.win') &&
  180. !document.getElementById('download-button')) {
  181.  
  182. const button = document.createElement('a');
  183. button.id = 'download-button';
  184. button.textContent = 'Download';
  185.  
  186. const imageUrl = window.location.href.split('?')[0];
  187. button.href = imageUrl;
  188.  
  189. const urlParams = new URLSearchParams(window.location.search);
  190. const originalFilename = urlParams.get('filename') || extractFilenameFromUrl(imageUrl);
  191.  
  192. button.download = originalFilename;
  193.  
  194. document.body.classList.add('has-download-button');
  195. document.body.appendChild(button);
  196.  
  197. button.addEventListener('click', event => {
  198. event.preventDefault();
  199. downloadImage(imageUrl, originalFilename);
  200. });
  201. }
  202. }
  203.  
  204. if (window.location.hostname === 'desuarchive.org' || window.location.hostname === 'archive.palanq.win') {
  205. // Add search field
  206. if (!document.getElementById('filename-search-container')) {
  207. const searchContainer = createSearchInterface();
  208. document.body.appendChild(searchContainer);
  209. }
  210.  
  211. // Direct event listeners without delegation
  212. document.querySelectorAll('a.post_file_filename').forEach(link => {
  213. link.addEventListener('click', event => {
  214. event.preventDefault();
  215. const imageUrl = link.closest('a').href;
  216. const originalFilename = getFullFilename(link);
  217. downloadImage(imageUrl, originalFilename);
  218. });
  219. });
  220.  
  221. document.querySelectorAll('a[href*="//desu-usergeneratedcontent.xyz/"] i.icon-download-alt, a[href*="//archive-media.palanq.win/"] i.icon-download-alt').forEach(button => {
  222. button.closest('a').addEventListener('click', event => {
  223. event.preventDefault();
  224. const imageUrl = button.closest('a').href;
  225. let filenameElement = button.closest('div.post_file, article.thread, article.post')?.querySelector('a.post_file_filename');
  226. if (!filenameElement) return;
  227.  
  228. const originalFilename = getFullFilename(filenameElement);
  229. downloadImage(imageUrl, originalFilename);
  230. });
  231. });
  232.  
  233. document.querySelectorAll('a[href*="//desu-usergeneratedcontent.xyz/"] img, a[href*="//archive-media.palanq.win/"] img').forEach(image => {
  234. image.closest('a').addEventListener('click', handleImageClick);
  235. });
  236. }
  237.  
  238. // Initial setup for direct image pages
  239. addDownloadButtonToImagePage();
  240.  
  241. // Simple observer for dynamic content
  242. const observer = new MutationObserver((mutations) => {
  243. for (const mutation of mutations) {
  244. if (mutation.addedNodes.length) {
  245. const newLinks = document.querySelectorAll('a.post_file_filename:not([data-handled])');
  246. newLinks.forEach(link => {
  247. link.dataset.handled = 'true';
  248. link.addEventListener('click', event => {
  249. event.preventDefault();
  250. const imageUrl = link.closest('a').href;
  251. const originalFilename = getFullFilename(link);
  252. downloadImage(imageUrl, originalFilename);
  253. });
  254. });
  255. }
  256. }
  257. });
  258.  
  259. observer.observe(document.body, { childList: true, subtree: true });
  260. })();