Pikabu Media Downloader (stable)

Add manual download buttons for images and videos in Pikabu story posts (individual or ZIP), using canvas to bypass CORS

// ==UserScript==
// @name         Pikabu Media Downloader (stable)
// @namespace    http://tampermonkey.net/
// @version      0.16
// @description  Add manual download buttons for images and videos in Pikabu story posts (individual or ZIP), using canvas to bypass CORS
// @author       stalkermiha
// @match        https://pikabu.ru/*
// @grant        none
// @license      MIT
// @require      https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// ==/UserScript==

(function() {
    'use strict';

    console.log('Script loaded at ' + new Date().toLocaleTimeString());

    // Общий метод для создания кнопок
    function createButton(text, color, clickHandler, wrapper) {
        const button = document.createElement('button');
        button.textContent = text;
        button.className = `${text.toLowerCase().replace(/ /g, '-')}-button`;
        button.style.cssText = `margin: 5px auto; padding: 10px 20px; background-color: ${color}; color: white; border: none; border-radius: 5px; cursor: pointer;`;
        button.addEventListener('click', () => clickHandler(wrapper));
        return button;
    }

    // Функция для скачивания отдельных изображений
    function downloadImages(wrapper) {
        console.log('Starting individual image download process for wrapper...');
        const images = wrapper.querySelectorAll('.story-image__image');
        if (!images.length) {
            console.log('No images found in this wrapper!');
            return;
        }

        console.log(`Found ${images.length} images in this wrapper.`);
        let delay = 0;
        images.forEach((img, index) => {
            if (img.complete && img.naturalHeight > 0) {
                setTimeout(() => {
                    console.log(`Downloading image ${img.src}`);
                    const canvas = document.createElement('canvas');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(img, 0, 0);
                    canvas.toBlob(blob => {
                        if (blob) {
                            const link = document.createElement('a');
                            link.href = URL.createObjectURL(blob);
                            link.download = `image_${index + 1}.png`;
                            document.body.appendChild(link);
                            link.click();
                            document.body.removeChild(link);
                        } else {
                            console.log(`Failed to create blob for image ${img.src}`);
                        }
                    }, 'image/png');
                }, delay);
                delay += 500;
            } else {
                console.log(`Skipping invalid image ${img.src}`);
            }
        });
    }

    // Функция для асинхронного скачивания архива изображений
    function downloadImagesAsZip(wrapper) {
        console.log('Starting ZIP download process for images in wrapper...');
        const images = wrapper.querySelectorAll('.story-image__image');
        if (!images.length) {
            console.log('No images found in this wrapper!');
            return;
        }

        console.log(`Found ${images.length} images in this wrapper for ZIP.`);
        const JSZip = window.JSZip;
        const zip = new JSZip();
        let processedCount = 0;

        images.forEach((img, index) => {
            if (img.complete && img.naturalHeight > 0) {
                const canvas = document.createElement('canvas');
                canvas.width = img.naturalWidth;
                canvas.height = img.naturalHeight;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
                canvas.toBlob(blob => {
                    if (blob) {
                        zip.file(`image_${index + 1}.png`, blob);
                        console.log(`Added image_${index + 1}.png to ZIP`);
                    } else {
                        console.log(`Failed to create blob for image ${img.src} in ZIP`);
                    }
                    processedCount++;
                    // Асинхронная генерация ZIP после обработки каждого изображения
                    if (processedCount === images.length) {
                        zip.generateAsync({ type: 'blob' }).then(content => {
                            console.log('ZIP generated, initiating download...');
                            const link = document.createElement('a');
                            link.href = URL.createObjectURL(content);
                            link.download = `pikabu_images_${Date.now()}.zip`;
                            document.body.appendChild(link);
                            link.click();
                            document.body.removeChild(link);
                        }).catch(err => console.error('Error generating ZIP:', err));
                    }
                }, 'image/png');
            } else {
                console.log(`Skipping invalid image ${img.src} for ZIP`);
                processedCount++;
                if (processedCount === images.length) {
                    zip.generateAsync({ type: 'blob' }).then(content => {
                        console.log('ZIP generated, initiating download...');
                        const link = document.createElement('a');
                        link.href = URL.createObjectURL(content);
                        link.download = `pikabu_images_${Date.now()}.zip`;
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);
                    }).catch(err => console.error('Error generating ZIP:', err));
                }
            }
        });
    }

    // Функция для скачивания отдельных видео
    function downloadVideos(wrapper) {
        console.log('Starting individual video download process for wrapper...');
        const players = wrapper.querySelectorAll('.player');
        if (!players.length) {
            console.log('No videos found in this wrapper!');
            return;
        }

        console.log(`Found ${players.length} videos in this wrapper.`);
        let delay = 0;
        players.forEach((player, index) => {
            const webmUrl = player.getAttribute('data-webm');
            const av1Url = player.getAttribute('data-av1');
            const videoUrl = webmUrl || av1Url;
            if (videoUrl) {
                setTimeout(() => {
                    console.log(`Downloading video ${videoUrl}`);
                    fetch(videoUrl)
                        .then(response => response.blob())
                        .then(blob => {
                            const link = document.createElement('a');
                            link.href = URL.createObjectURL(blob);
                            link.download = `video_${index + 1}.${webmUrl ? 'webm' : 'mp4'}`;
                            document.body.appendChild(link);
                            link.click();
                            document.body.removeChild(link);
                        })
                        .catch(err => console.error(`Failed to download video ${videoUrl}:`, err));
                }, delay);
                delay += 1000;
            } else {
                console.log(`No downloadable video format found for player ${index + 1}`);
            }
        });
    }

    // Функция для скачивания архива видео
    function downloadVideosAsZip(wrapper) {
        console.log('Starting ZIP download process for videos in wrapper...');
        const players = wrapper.querySelectorAll('.player');
        if (!players.length) {
            console.log('No videos found in this wrapper!');
            return;
        }

        console.log(`Found ${players.length} videos in this wrapper for ZIP.`);
        const JSZip = window.JSZip;
        const zip = new JSZip();
        let processedCount = 0;

        players.forEach((player, index) => {
            const webmUrl = player.getAttribute('data-webm');
            const av1Url = player.getAttribute('data-av1');
            const videoUrl = webmUrl || av1Url;
            if (videoUrl) {
                fetch(videoUrl)
                    .then(response => response.blob())
                    .then(blob => {
                        zip.file(`video_${index + 1}.${webmUrl ? 'webm' : 'mp4'}`, blob);
                        console.log(`Added video_${index + 1}.${webmUrl ? 'webm' : 'mp4'} to ZIP`);
                        processedCount++;
                        checkComplete();
                    })
                    .catch(err => {
                        console.error(`Failed to fetch video ${videoUrl}:`, err);
                        processedCount++;
                        checkComplete();
                    });
            } else {
                console.log(`No downloadable video format found for player ${index + 1}`);
                processedCount++;
                checkComplete();
            }
        });

        function checkComplete() {
            if (processedCount === players.length) {
                zip.generateAsync({ type: 'blob' }).then(content => {
                    console.log('ZIP generated, initiating download...');
                    const link = document.createElement('a');
                    link.href = URL.createObjectURL(content);
                    link.download = `pikabu_videos_${Date.now()}.zip`;
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                }).catch(err => console.error('Error generating ZIP:', err));
            }
        }
    }

    function addDownloadButtons(wrapper) {
        if (wrapper.dataset.buttonsAdded) return; // Пропускаем, если кнопки уже добавлены

        const images = wrapper.querySelectorAll('.story-image__image');
        const players = wrapper.querySelectorAll('.player');
        const lastBlock = wrapper.querySelector('.story-block:last-of-type');
        if (!lastBlock) {
            console.log('No last block found in wrapper!');
            return;
        }

        // Удаляем существующие кнопки, чтобы избежать дублирования
        const existingButtons = lastBlock.querySelectorAll('.download-button, .zip-button, .download-video-button, .zip-video-button');
        existingButtons.forEach(btn => btn.remove());

        console.log('Adding download buttons to wrapper...');

        // Кнопки для изображений, если есть
        if (images.length > 0) {
            const singleButton = createButton('Download Images', '#007bff', downloadImages, wrapper);
            const zipButton = createButton('Download Images as ZIP', '#28a745', downloadImagesAsZip, wrapper);
            lastBlock.appendChild(singleButton);
            lastBlock.appendChild(zipButton);
        }

        // Кнопки для видео, если есть
        if (players.length > 0) {
            const videoButton = createButton('Download Videos', '#dc3545', downloadVideos, wrapper);
            const zipVideoButton = createButton('Download Videos as ZIP', '#17a2b8', downloadVideosAsZip, wrapper);
            lastBlock.appendChild(videoButton);
            lastBlock.appendChild(zipVideoButton);
        }

        wrapper.dataset.buttonsAdded = 'true'; // Отмечаем, что кнопки добавлены
    }

    // Наблюдатель за появлением блоков контента
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                const wrappers = document.querySelectorAll('.story__content-wrapper:not([data-buttons-added])');
                wrappers.forEach(wrapper => {
                    console.log('Detected new content wrapper, adding buttons...');
                    addDownloadButtons(wrapper);
                });
            }
        });
    });

    // Запуск скрипта сразу после загрузки зависимостей
    console.log('Loading dependencies...');
    const initialWrappers = document.querySelectorAll('.story__content-wrapper');
    initialWrappers.forEach(wrapper => addDownloadButtons(wrapper));
    observer.observe(document.body, { childList: true, subtree: true });

    const css = document.createElement('style');
    css.innerHTML = `
        .download-button:hover { background-color: #0056b3; }
        .zip-button:hover { background-color: #218838; }
        .download-video-button:hover { background-color: #c82333; }
        .zip-video-button:hover { background-color: #138496; }
    `;
    document.head.appendChild(css);
})();