Simfileshare Folder Downloader

Adds a button to download all files in a Simfileshare folder as a zip file and individual download buttons

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Simfileshare Folder Downloader
// @namespace    https://github.com/dear-clouds/mio-userscripts
// @version      1.0
// @description  Adds a button to download all files in a Simfileshare folder as a zip file and individual download buttons
// @author       Mio.
// @supportURL   https://github.com/dear-clouds/mio-userscripts/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=simfileshare.net
// @match        *://*.simfileshare.net/folder/*
// @grant        none
// @license      GPL-3.0
// @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', () => {
        const MAX_CONCURRENT_DOWNLOADS = 8; // Control how many files to download concurrently here

        // console.log('Page loaded, initializing script...');

        // Button Download All
        const downloadAllButton = document.createElement('button');
        downloadAllButton.innerHTML = 'Download All Files as ZIP';
        const loaderSpanAll = document.createElement('span');
        loaderSpanAll.className = 'loader';
        loaderSpanAll.style.display = 'none';
        downloadAllButton.appendChild(loaderSpanAll);
        downloadAllButton.style.padding = '10px';
        downloadAllButton.style.backgroundColor = '#4CAF50';
        downloadAllButton.style.color = 'white';
        downloadAllButton.style.border = 'none';
        downloadAllButton.style.cursor = 'pointer';
        downloadAllButton.style.marginBottom = '10px';
        downloadAllButton.style.position = 'relative';

        const h4Element = document.querySelector('h4');
        if (h4Element) {
            h4Element.parentNode.insertBefore(downloadAllButton, h4Element.nextSibling);
            // console.log('Download All button appended successfully');
        }

        const loaderStyle = document.createElement('style');
        loaderStyle.innerHTML = `
            .loader {
                border: 4px solid #f3f3f3;
                border-top: 4px solid #3498db;
                border-radius: 50%;
                width: 16px;
                height: 16px;
                animation: spin 1s linear infinite;
                position: relative;
                margin-left: 10px;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            .dl-button {
                margin-left: 10px;
                padding: 4px 8px;
                background-color: #3498db;
                color: white;
                border: none;
                cursor: pointer;
                font-size: 0.9em;
                position: relative;
            }
        `;
        document.head.appendChild(loaderStyle);
        // console.log('Loader styles added');

        downloadAllButton.addEventListener('click', async () => {
            console.log('Download All button clicked');

            // Get all the download links in the folder
            const downloadLinks = document.querySelectorAll('a[href*="/download/"]');
            console.log(`Found ${downloadLinks.length} download links`);

            if (downloadLinks.length === 0) {
                alert('No files found to download!');
                return;
            }

            loaderSpanAll.style.display = 'inline-block';
            // console.log('Loader displayed for Download All button');

            // Create a ZIP file with JSZip
            const zip = new JSZip();
            const folderTitle = h4Element.textContent.replace('Folder: ', '').trim();
            console.log(`Folder title determined: ${folderTitle}`);

            // Function to fetch files with retries and concurrency limit
            const fetchFileWithRetry = async (url, retries = 20) => { // Here you can control the number of retries. Default: 20
                for (let attempt = 0; attempt < retries; attempt++) {
                    try {
                        const directUrl = url.replace('/download/', '/cdn/download/') + '/?dl';
                        console.log(`Fetching: ${directUrl} (Attempt ${attempt + 1})`);
                        const response = await fetch(directUrl);
                        if (response.ok) {
                            console.log(`Successfully fetched: ${directUrl}`);
                            return await response.blob();
                        } else {
                            console.warn(`Attempt ${attempt + 1} failed: ${response.statusText}`);
                        }
                    } catch (error) {
                        console.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
                    }
                    await new Promise(resolve => setTimeout(resolve, 1000)); // Wait before retrying
                }
                throw new Error(`Failed to fetch file after ${retries} attempts: ${url}`);
            };

            // Concurrency limiting function
            const limitConcurrency = async (limit, tasks) => {
                const results = [];
                const executing = [];

                for (const task of tasks) {
                    const p = task().then(result => {
                        executing.splice(executing.indexOf(p), 1);
                        return result;
                    });
                    results.push(p);
                    executing.push(p);

                    if (executing.length >= limit) {
                        await Promise.race(executing);
                    }
                }
                return Promise.all(results);
            };

            // Prepare a list of tasks to fetch each file
            const tasks = Array.from(downloadLinks).map(link => async () => {
                try {
                    console.log(`Preparing to fetch file: ${link.href}`);
                    const blob = await fetchFileWithRetry(link.href);
                    const filename = link.textContent.trim() || `file_${Math.random().toString(36).substring(7)}`;
                    console.log(`Adding file to ZIP: ${filename}`);
                    zip.file(filename, blob);
                } catch (error) {
                    console.error(error.message);
                }
            });

            // Run tasks with controlled concurrency
            await limitConcurrency(MAX_CONCURRENT_DOWNLOADS, tasks);
            // console.log('All files fetched, generating ZIP');

            // Generate ZIP and trigger download
            zip.generateAsync({ type: "blob" }).then(content => {
                saveAs(content, `${folderTitle}.zip`);
                loaderSpanAll.style.display = 'none';
                console.log('ZIP generated and download triggered');
            }).catch(error => {
                console.error('Error generating ZIP:', error);
                loaderSpanAll.style.display = 'none';
                alert('Error occurred while generating ZIP.');
            });
        });

        // Add individual download buttons next to each file link
        const fileLinks = document.querySelectorAll('tr td a[href*="/download/"]');
        fileLinks.forEach(link => {
            const dlButton = document.createElement('button');
            dlButton.innerHTML = 'DL';
            const loaderSpan = document.createElement('span');
            loaderSpan.className = 'loader';
            loaderSpan.style.display = 'none';
            dlButton.appendChild(loaderSpan);
            dlButton.className = 'dl-button';
            dlButton.addEventListener('click', async (e) => {
                e.preventDefault();
                console.log(`Individual download button clicked for link: ${link.href}`);
                loaderSpan.style.display = 'inline-block';
                // console.log('Loader displayed for individual download button');
                try {
                    // First visit the link to prepare the direct download
                    await fetch(link.href, { method: 'GET', mode: 'no-cors' });
                    const directUrl = link.href.replace('/download/', '/cdn/download/') + '/?dl';
                    console.log(`Fetching direct URL: ${directUrl}`);
                    const response = await fetch(directUrl);
                    if (!response.ok) {
                        throw new Error(`Failed to download: ${response.statusText}`);
                    }
                    const blob = await response.blob();
                    const filename = link.textContent.trim();
                    console.log(`Successfully downloaded file: ${filename}`);
                    const a = document.createElement('a');
                    a.href = URL.createObjectURL(blob);
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    console.log(`File download triggered: ${filename}`);
                } catch (error) {
                    console.error('Error downloading file:', error);
                    alert(`Error downloading file: ${link.textContent.trim()}`);
                } finally {
                    loaderSpan.style.display = 'none';
                    // console.log('Loader hidden for individual download button');
                }
            });
            link.parentNode.appendChild(dlButton);
            // console.log('DL button appended for link:', link.textContent);
        });
    });
})();