MediaZipper

Download images, audio, and video files from a webpage into a ZIP file with filters and draggable UI panel.

// ==UserScript==
// @name         MediaZipper
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Download images, audio, and video files from a webpage into a ZIP file with filters and draggable UI panel.
// @author       Kanha Pise
// @match        *://*/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==

(function waitForLibraries() {
    if (typeof JSZip === 'undefined' || typeof window.saveAs === 'undefined') {
        console.log('Waiting for JSZip and FileSaver...');
        setTimeout(waitForLibraries, 100);
        return;
    }

    (function () {
        'use strict';

        const panel = document.createElement('div');
        panel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            width: 220px;
            background: white;
            border: 1px solid #ccc;
            padding: 10px;
            z-index: 9999;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
            font-family: sans-serif;
        `;
        panel.innerHTML = `
            <div id="mz-header" style="cursor: move; font-weight: bold; margin-bottom: 5px;">📦 MediaZipper</div>
            <div id="mz-body">
                <label><input type="checkbox" id="filterJPG" checked> .jpg</label><br>
                <label><input type="checkbox" id="filterPNG" checked> .png</label><br>
                <label><input type="checkbox" id="filterMP3" checked> .mp3</label><br>
                <label><input type="checkbox" id="filterMP4" checked> .mp4</label><br>
                <button id="downloadBtn" style="margin-top: 8px; width: 100%;">Download ZIP</button>
                <div id="progress" style="margin-top: 10px; display: none;">
                    <progress id="progressBar" value="0" max="100" style="width: 100%;"></progress>
                    <span id="progressText">0%</span>
                </div>
            </div>
            <button id="toggleBtn" style="margin-top: 5px; width: 100%;">Collapse</button>
        `;
        document.body.appendChild(panel);

        // Draggable Panel
        const header = document.getElementById('mz-header');
        let isDragging = false, offsetX, offsetY;

        header.addEventListener('mousedown', function (e) {
            isDragging = true;
            offsetX = e.clientX - panel.offsetLeft;
            offsetY = e.clientY - panel.offsetTop;
            e.preventDefault();
        });

        document.addEventListener('mousemove', function (e) {
            if (isDragging) {
                panel.style.left = `${e.clientX - offsetX}px`;
                panel.style.top = `${e.clientY - offsetY}px`;
            }
        });

        document.addEventListener('mouseup', function () {
            isDragging = false;
        });

        // Toggle Collapse
        const toggleBtn = document.getElementById('toggleBtn');
        const body = document.getElementById('mz-body');
        toggleBtn.onclick = function () {
            const hidden = body.style.display === 'none';
            body.style.display = hidden ? 'block' : 'none';
            toggleBtn.textContent = hidden ? 'Collapse' : 'Expand';
        };

        // Main ZIP download function (Promise-based)
        function fetchAndZipMedia() {
            const zip = new JSZip();
            const filters = {
                jpg: document.getElementById('filterJPG').checked,
                png: document.getElementById('filterPNG').checked,
                mp3: document.getElementById('filterMP3').checked,
                mp4: document.getElementById('filterMP4').checked
            };

            const mediaElements = Array.from(document.querySelectorAll('img[src], audio[src], video[src], source[src]'));
            const mediaSrcs = new Set();

            const progress = document.getElementById('progress');
            const progressBar = document.getElementById('progressBar');
            const progressText = document.getElementById('progressText');
            progress.style.display = 'block';

            let completed = 0;
            let addedCount = 0;

            function updateProgress() {
                const percent = Math.round((completed / mediaElements.length) * 100);
                progressBar.value = percent;
                progressText.textContent = percent + '%';
            }

            const fetchPromises = mediaElements.map(function (el) {
                let src = el.src || el.currentSrc || el.getAttribute('src');
                if (!src || mediaSrcs.has(src)) {
                    completed++;
                    updateProgress();
                    return Promise.resolve(); // Skip duplicate or empty src
                }
                mediaSrcs.add(src);

                const ext = src.split('.').pop().split('?')[0].toLowerCase();
                if ((ext === 'jpg' && !filters.jpg) ||
                    (ext === 'png' && !filters.png) ||
                    (ext === 'mp3' && !filters.mp3) ||
                    (ext === 'mp4' && !filters.mp4)) {
                    completed++;
                    updateProgress();
                    return Promise.resolve();
                }

                return fetch(src, { mode: 'cors' }).then(function (res) {
                    return res.blob();
                }).then(function (blob) {
                    const filename = decodeURIComponent(src.split('/').pop().split('?')[0]).replace(/[\\/:*?"<>|]/g, '_');
                    zip.file(filename, blob);
                    addedCount++;
                }).catch(function (err) {
                    console.warn('Failed to fetch:', src, err);
                }).finally(function () {
                    completed++;
                    updateProgress();
                });
            });

            Promise.all(fetchPromises).then(function () {
                if (addedCount === 0) {
                    alert("No media files matched the selected filters or were blocked by CORS.");
                    progress.style.display = 'none';
                    return;
                }
                zip.generateAsync({ type: 'blob' }).then(function (content) {
                    window.saveAs(content, 'media.zip');
                    progress.style.display = 'none';
                }).catch(function (err) {
                    alert("Failed to generate ZIP file.");
                    console.error(err);
                    progress.style.display = 'none';
                });
            });
        }

        document.getElementById('downloadBtn').addEventListener('click', fetchAndZipMedia);
    })();
})();