// ==UserScript==
// @name Desu Image Downloader
// @version 4.0
// @description Download images with original filenames on desuarchive.org, archive.palanq.win, and add download button to direct image pages
// @author Anonimas
// @match https://desuarchive.org/*
// @match https://desu-usergeneratedcontent.xyz/*
// @match https://archive.palanq.win/*
// @match https://archive-media.palanq.win/*
// @grant GM_download
// @grant GM_addStyle
// @namespace https://greasyfork.org/users/1342214
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
#filename-search-container {
position: fixed !important;
bottom: 20px !important;
right: 20px !important;
display: flex !important;
align-items: center !important;
background-color: rgba(0, 0, 0, 0.5) !important;
border-radius: 8px !important;
padding: 0 8px !important;
transition: background-color 0.3s !important;
z-index: 9998 !important;
height: 44px !important;
box-sizing: border-box !important;
}
#filename-search-container:hover {
background-color: rgba(0, 0, 0, 0.7) !important;
}
#filename-search-input {
background-color: transparent !important;
border: none !important;
color: white !important;
font-size: 18px !important;
padding: 0 12px !important;
width: 250px !important;
height: 100% !important;
outline: none !important;
font-family: Arial, sans-serif !important;
line-height: 44px !important;
margin: 0 !important;
box-shadow: none !important;
}
#filename-search-input::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
}
#filename-search-input:focus {
outline: none !important;
box-shadow: none !important;
border: none !important;
background-color: transparent !important;
}
#filename-search-button {
background-color: transparent !important;
color: white !important;
border: none !important;
padding: 0 16px !important;
height: 100% !important;
cursor: pointer !important;
font-size: 18px !important;
font-family: Arial, sans-serif !important;
transition: background-color 0.3s !important;
line-height: 44px !important;
margin: 0 !important;
}
#filename-search-button:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
border-radius: 5px !important;
}
#download-button {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
text-decoration: none;
font-family: Arial, sans-serif;
z-index: 9999;
display: none; /* Hidden by default */
}
#download-button:hover {
background-color: rgba(0, 0, 0, 0.7);
}
body.has-download-button #filename-search-container {
right: 140px !important;
}
`);
// Helper function to get full filename from an element
function getFullFilename(element) {
return element?.getAttribute('title') || element?.textContent?.trim() || null;
}
//Helper Function to extract filename from a URL.
function extractFilenameFromUrl(url) {
try {
const parsedUrl = new URL(url);
const pathname = parsedUrl.pathname;
return pathname.substring(pathname.lastIndexOf('/') + 1);
} catch (e) {
console.error("Error parsing URL", url, e);
return null;
}
}
//Helper function to append the filename to the url.
function appendFilenameToUrl(url, filename) {
try {
const parsedUrl = new URL(url);
parsedUrl.searchParams.set('filename', filename);
return parsedUrl.toString();
}
catch(e) {
console.error("Error modifying URL", url, e);
return url;
}
}
// Function to download a single image with GM_download
function downloadImage(imageUrl, originalFilename) {
if (!imageUrl || !originalFilename) {
console.error("Invalid image URL or filename:", { imageUrl, originalFilename });
return;
}
GM_download({
url: imageUrl,
name: originalFilename,
onload: () => {},
onerror: (error) => console.error('Download error:', error)
});
}
// Function to handle image click (opening image in new tab with filename)
function handleImageClick(event) {
event.preventDefault(); // Prevent the default link behavior
const imageLink = event.target.closest('a[href*="//desu-usergeneratedcontent.xyz/"], a[href*="//archive-media.palanq.win/"]');
if (!imageLink) return; // Exit if no image link is found
const imageUrl = imageLink.href;
let filenameElement = imageLink.closest('div.post_file, article.thread, article.post')?.querySelector('a.post_file_filename');
if (!filenameElement) return;
const originalFilename = getFullFilename(filenameElement);
const newUrl = appendFilenameToUrl(imageUrl, originalFilename);
window.open(newUrl, '_blank');
}
// Function to create the search interface
function createSearchInterface() {
const searchContainer = document.createElement('div');
searchContainer.id = 'filename-search-container';
const searchInput = document.createElement('input');
searchInput.id = 'filename-search-input';
searchInput.type = 'text';
searchInput.placeholder = 'Search filename...';
searchInput.autocomplete = 'off';
const searchButton = document.createElement('button');
searchButton.id = 'filename-search-button';
searchButton.textContent = 'Search';
const performSearch = () => {
const searchTerm = searchInput.value.trim();
if (!searchTerm) return;
let searchUrl;
const currentBoard = window.location.pathname.split('/')[1] || 'a';
if (window.location.hostname === 'archive.palanq.win') {
searchUrl = `https://archive.palanq.win/${currentBoard}/search/filename/${encodeURIComponent(searchTerm)}/`;
} else {
searchUrl = `https://desuarchive.org/${currentBoard}/search/filename/${encodeURIComponent(searchTerm)}/`;
}
window.location.href = searchUrl;
};
searchButton.addEventListener('click', performSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
searchContainer.appendChild(searchInput);
searchContainer.appendChild(searchButton);
return searchContainer;
}
// Function to add the download button to direct image pages
function addDownloadButtonToImagePage() {
if (!(window.location.hostname === 'desu-usergeneratedcontent.xyz' || window.location.hostname === 'archive-media.palanq.win')) {
return; // Exit if not on an image page
}
if (document.getElementById('download-button')) {
return;
}
const button = document.createElement('a');
button.id = 'download-button';
button.textContent = 'Download';
const imageUrl = window.location.href.split('?')[0];
button.href = imageUrl;
const urlParams = new URLSearchParams(window.location.search);
const originalFilename = urlParams.get('filename') || extractFilenameFromUrl(imageUrl);
button.download = originalFilename;
document.body.classList.add('has-download-button');
document.body.appendChild(button);
button.addEventListener('click', event => {
event.preventDefault();
downloadImage(imageUrl, originalFilename);
});
//Make download button visable
button.style.display = 'block';
}
// Event delegation for image downloads and filename handling
function setupEventDelegation() {
document.body.addEventListener('click', function(event) {
const target = event.target;
//Direct Download from File Name
if(target.closest('a.post_file_filename')) {
event.preventDefault();
const link = target.closest('a.post_file_filename');
if (!link) return;
const imageUrl = link.href;
const originalFilename = getFullFilename(link);
downloadImage(imageUrl,originalFilename);
return;
}
//Direct Download from Icon
if (target.closest('a[href*="//desu-usergeneratedcontent.xyz/"] i.icon-download-alt, a[href*="//archive-media.palanq.win/"] i.icon-download-alt')) {
event.preventDefault();
const downloadButton = target.closest('a');
if (!downloadButton) return;
const imageUrl = downloadButton.href;
let filenameElement = downloadButton.closest('div.post_file, article.thread, article.post')?.querySelector('a.post_file_filename');
if (!filenameElement) return;
const originalFilename = getFullFilename(filenameElement);
downloadImage(imageUrl,originalFilename);
return;
}
//Handle image click
if (target.closest('a[href*="//desu-usergeneratedcontent.xyz/"] img, a[href*="//archive-media.palanq.win/"] img')) {
handleImageClick(event);
}
});
}
// Initialize
function initialize() {
if (window.location.hostname === 'desuarchive.org' || window.location.hostname === 'archive.palanq.win') {
if (!document.getElementById('filename-search-container')) {
const searchContainer = createSearchInterface();
document.body.appendChild(searchContainer);
}
setupEventDelegation();
}
addDownloadButtonToImagePage();
// Setup observer for dynamic content
const observer = new MutationObserver(debounce(handleMutations, 200));
observer.observe(document.body, { childList: true, subtree: true });
}
// Mutation Handling
function handleMutations(mutations) {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
const newLinks = document.querySelectorAll('a.post_file_filename:not([data-handled])');
newLinks.forEach(link => {
link.dataset.handled = 'true';
});
}
}
}
//Debounce Function
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
}
initialize();
})();