Patreon Attachment Downloader

Download all Patreon post attachments as a ZIP file

// ==UserScript==
// @name         Patreon Attachment Downloader
// @namespace    https://github.com/dear-clouds/mio-userscripts
// @version      1.0
// @description  Download all Patreon post attachments as a ZIP file
// @author       Mio.
// @supportURL   https://github.com/dear-clouds/mio-userscripts/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=patreon.com
// @license      GPL-3.0
// @match        *://*.patreon.com/posts/*
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==

(function() {
    'use strict';
    
    window.addEventListener('load', function() {
        const attachmentContainer = document.querySelector('div[data-tag="post-attachments"]');
        if (attachmentContainer) {
            const downloadButton = document.createElement('button');
            downloadButton.innerText = 'Download All Attachments as ZIP';
            downloadButton.style.marginTop = '10px';
            downloadButton.style.padding = '10px';
            downloadButton.style.backgroundColor = 'var(--component-button-action-default)';
            downloadButton.style.color = 'var(--component-button-onAction-default)';
            downloadButton.style.border = 'none';
            downloadButton.style.borderRadius = 'var(--global-radius-md)';
            downloadButton.style.cursor = 'pointer';

            downloadButton.addEventListener('click', function() {
                downloadAttachmentsAsZip(downloadButton);
            });
            attachmentContainer.appendChild(downloadButton);
        }
    });

    function downloadAttachmentsAsZip(downloadButton) {
        const attachmentLinks = Array.from(document.querySelectorAll('a[data-tag="post-attachment-link"]'));
        if (attachmentLinks.length === 0) {
            alert('No attachments found on this page.');
            return;
        }

        // Disable the button and indicate loading
        downloadButton.disabled = true;
        downloadButton.style.cursor = 'not-allowed';
        downloadButton.innerText = 'Downloading...';

        const zip = new JSZip();
        let downloaded = 0;
        const postTitleElement = document.querySelector('span[data-tag="post-title"]');
        const postTitle = postTitleElement ? postTitleElement.innerText.trim().replace(/[^a-zA-Z0-9_\-\s]/g, '') : 'patreon_attachments';
        const zipFilename = `${postTitle}.zip`;

        attachmentLinks.forEach((link) => {
            const url = link.href;
            const filename = link.innerText.trim().replace(/[^a-zA-Z0-9_\-\.\s]/g, '') || `attachment_${downloaded + 1}`;

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: function(response) {
                    zip.file(filename, response.response);
                    downloaded++;

                    if (downloaded === attachmentLinks.length) {
                        zip.generateAsync({ type: 'blob' }).then(function(content) {
                            saveAs(content, zipFilename);
                            // Re-enable the button and reset text
                            downloadButton.disabled = false;
                            downloadButton.style.cursor = 'pointer';
                            downloadButton.innerText = 'Download All Attachments as ZIP';
                        });
                    }
                },
                onerror: function() {
                    alert('Failed to download an attachment.');
                    // Re-enable the button and reset text if an error occurs
                    downloadButton.disabled = false;
                    downloadButton.style.cursor = 'pointer';
                    downloadButton.innerText = 'Download All Attachments as ZIP';
                }
            });
        });
    }
})();