Greasy Fork 支持简体中文。

Desu Image Downloader

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

// ==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();
})();