Desu Image Downloader

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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