Moodle Resourcen Downloader

Fügt "Alle herunterladen"-Buttons hinzu, um alle Ressourcen in einem Moodle-Kurs herunterzuladen.

// ==UserScript==
// @name          Moodle Resourcen Downloader
// @namespace     http://rococolabs.org
// @description   Fügt "Alle herunterladen"-Buttons hinzu, um alle Ressourcen in einem Moodle-Kurs herunterzuladen.
// @version       2.1
// @run-at        document-end
// @match         https://moodle.thi.de/course/view.php*
// @grant         none
// @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
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Utility function to clean special characters from strings
    function cleanSpecialCharacters(string) {
        return string.replace(/[\u00E0-\u00FC]/g, (match) => {
            const specialChars = {
                'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u', 'ñ': 'n', 'ç': 'c',
                'à': 'a', 'è': 'e', 'ì': 'i', 'ò': 'o', 'ù': 'u',
                'ä': 'a', 'ë': 'e', 'ï': 'i', 'ö': 'o', 'ü': 'u'
            };
            return specialChars[match] || '';
        }).replace(/[^\w\s]/gi, '').replace(/\s+/g, '_');
    }

    // Function to fetch the course name
    function fetchCourseName() {
        const courseTitleElement = document.querySelector('.page-header-headings h1');
        return courseTitleElement ? cleanSpecialCharacters(courseTitleElement.innerText) : 'Course';
    }

    // Function to create and show loading spinner and status message
    function showLoadingSpinner() {
        const overlay = document.createElement('div');
        overlay.id = 'loading-overlay';
        overlay.style.position = 'fixed';
        overlay.style.top = '0';
        overlay.style.left = '0';
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        overlay.style.display = 'flex';
        overlay.style.flexDirection = 'column';
        overlay.style.justifyContent = 'center';
        overlay.style.alignItems = 'center';
        overlay.style.zIndex = '10000';
        document.body.appendChild(overlay);

        const spinner = document.createElement('div');
        spinner.id = 'loading-spinner';
        spinner.style.border = '16px solid rgba(0, 0, 0, 0)';
        spinner.style.borderTop = '16px solid #00ffcc';
        spinner.style.borderRadius = '50%';
        spinner.style.width = '120px';
        spinner.style.height = '120px';
        spinner.style.animation = 'spin 2s linear infinite';
        overlay.appendChild(spinner);

        const status = document.createElement('div');
        status.id = 'status-message';
        status.style.color = '#00ffcc';
        status.style.fontFamily = 'monospace';
        status.style.backgroundColor = 'transparent';
        status.style.padding = '10px';
        status.style.border = '2px solid #00ffcc';
        status.style.borderRadius = '0';
        status.style.textAlign = 'center';
        status.style.marginTop = '20px';
        overlay.appendChild(status);

        const style = document.createElement('style');
        style.textContent = `
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
        `;
        document.head.appendChild(style);
    }

    // Function to update status message
    function updateStatusMessage(message) {
        const status = document.getElementById('status-message');
        if (status) {
            status.textContent = message;
        }
    }

    // Function to hide loading spinner and status message
    function hideLoadingSpinner() {
        const overlay = document.getElementById('loading-overlay');
        if (overlay) {
            overlay.remove();
        }
    }

    // Function to get file extension from URL
    function getFileExtension(url) {
        const pathname = new URL(url).pathname;
        const basename = pathname.split('/').pop();
        const match = basename.match(/\.([a-zA-Z0-9]+)$/);
        return match ? match[1] : '';
    }

    // Function to download and zip all resources using JSZip
    async function downloadAllResources(group, zip) {
        const links = document.querySelectorAll(`#${group} .activity.resource a, #${group} .activity.folder a`);
        for (const link of links) {
            const href = link.getAttribute('href');
            if (href && !href.includes('session-')) {
                try {
                    updateStatusMessage(`Hole ${link.innerText.trim()}...`);
                    const response = await fetch(href);
                    const text = await response.text();
                    const doc = new DOMParser().parseFromString(text, 'text/html');
                    const downloadLink = doc.querySelector('.resourceworkaround a')?.href || href;
                    const filenameBase = cleanSpecialCharacters(link.innerText.trim()) || 'download';
                    const fileExtension = getFileExtension(downloadLink);
                    const filename = `${filenameBase}.${fileExtension}`;
                    updateStatusMessage(`Lade ${filename} herunter...`);
                    const fileResponse = await fetch(downloadLink);
                    const fileBlob = await fileResponse.blob();
                    zip.file(filename, fileBlob, {binary: true});

                    // If it's a folder, recursively download its contents
                    if (link.closest('.modtype_folder')) {
                        await downloadFolderContents(href, filenameBase, zip);
                    }

                } catch (error) {
                    console.error(`Fehler beim Herunterladen der Ressource von ${href}:`, error);
                }
            }
        }
    }

    // Function to download contents of a folder
    async function downloadFolderContents(folderUrl, parentFolderName, zip) {
        try {
            const response = await fetch(folderUrl);
            const text = await response.text();
            const doc = new DOMParser().parseFromString(text, 'text/html');
            const folderLinks = doc.querySelectorAll('.fp-filename-icon a');

            for (const link of folderLinks) {
                const href = link.getAttribute('href');
                if (href && !href.includes('session-')) {
                    const filenameBase = cleanSpecialCharacters(link.querySelector('.fp-filename').innerText.trim()) || 'download';
                    const fileExtension = getFileExtension(href);
                    const filename = `${parentFolderName}/${filenameBase}.${fileExtension}`;
                    updateStatusMessage(`Lade ${filename} herunter...`);
                    const fileResponse = await fetch(href);
                    const fileBlob = await fileResponse.blob();
                    zip.file(filename, fileBlob, {binary: true});
                }
            }
        } catch (error) {
            console.error(`Fehler beim Herunterladen des Verzeichnisses von ${folderUrl}:`, error);
        }
    }

    // Function to download and zip all resources from the entire course using JSZip
    async function downloadAllCourseResources() {
        const zip = new JSZip();
        const sections = document.querySelectorAll('.topics>li.section.main');
        const courseName = fetchCourseName();
        showLoadingSpinner();
        updateStatusMessage('Download startet...');
        for (const section of sections) {
            const group = section.getAttribute('id');
            await downloadAllResources(group, zip);
        }
        updateStatusMessage('Erstelle ZIP...');
        zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }).then(function(content) {
            hideLoadingSpinner();
            saveAs(content, `${courseName}.zip`);
        }).catch(function(error) {
            hideLoadingSpinner();
            console.error('Fehler beim Erstellen der ZIP-Datei:', error);
        });
    }

    // Button styling
    function createStyledButton(id, text) {
        const button = document.createElement('button');
        button.id = id;
        button.textContent = text;
        button.style.backgroundColor = '#1d1d1d';
        button.style.color = '#00ffcc';
        button.style.border = '2px solid #00ffcc';
        button.style.padding = '8px 20px';
        button.style.textAlign = 'center';
        button.style.textDecoration = 'none';
        button.style.display = 'inline-block';
        button.style.fontSize = '14px';
        button.style.margin = '4px 2px';
        button.style.cursor = 'pointer';
        button.style.borderRadius = '0';
        button.style.fontFamily = 'monospace';
        button.style.boxShadow = '0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)';

        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = '#00ffcc';
            button.style.color = '#1d1d1d';
        });

        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = '#1d1d1d';
            button.style.color = '#00ffcc';
        });

        return button;
    }

    // Inject buttons into the page
    function addDownloadButtons() {
        // Add course-wide download button
        const courseHeader = document.querySelector('.page-header-headings');
        if (courseHeader) {
            const downloadAllCourseButton = createStyledButton('triggerall-course', 'Alle Kurs-Ressourcen herunterladen');
            downloadAllCourseButton.addEventListener('click', (e) => {
                e.preventDefault();
                downloadAllCourseResources();
            });
            const courseButtonContainer = document.createElement('div');
            courseButtonContainer.style.display = 'flex';
            courseButtonContainer.style.justifyContent = 'left';
            courseButtonContainer.style.marginBottom = '20px';
            courseButtonContainer.appendChild(downloadAllCourseButton);
            courseHeader.appendChild(courseButtonContainer);
        }

        // Add section-specific download buttons
        const sections = document.querySelectorAll('.topics>li.section.main');
        const courseName = fetchCourseName();
        sections.forEach(section => {
            const group = section.getAttribute('id');
            const downloadAllButton = createStyledButton(`triggerall-${group}`, 'Alle Ressourcen herunterladen');
            downloadAllButton.addEventListener('click', async (e) => {
                e.preventDefault();
                const zip = new JSZip();
                showLoadingSpinner();
                updateStatusMessage('Download startet...');
                await downloadAllResources(group, zip);
                updateStatusMessage('Erstelle ZIP...');
                zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }).then(function(content) {
                    hideLoadingSpinner();
                    saveAs(content, `${courseName}_${group}.zip`);
                }).catch(function(error) {
                    hideLoadingSpinner();
                    console.error('Fehler beim Erstellen der ZIP-Datei:', error);
                });
            });

            const downloadContainer = document.createElement('div');
            downloadContainer.style.display = 'flex';
            downloadContainer.style.justifyContent = 'flex-end';
            downloadContainer.style.marginTop = '10px';
            downloadContainer.appendChild(downloadAllButton);

            section.querySelector('.content').appendChild(downloadContainer);
        });
    }

    addDownloadButtons();
})();