Pawchive File Ripper

Fast, lightweight downloader for Pawchive. ZIP bundle generation using fflate.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

Advertisement:

// ==UserScript==
// @name         Pawchive File Ripper
// @namespace    Tampermonkey/Violentmonkey Scripts
// @version      1.0
// @description  Fast, lightweight downloader for Pawchive. ZIP bundle generation using fflate.
// @author       Selentia-IX
// @icon         https://raw.githubusercontent.com/Selentia-IX/PawchiveRipper/main/pwr-logo.png
// @license      GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @match        https://pawchive.st/*
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://unpkg.com/[email protected]/umd/index.js
// @grant        GM_xmlhttpRequest
// @connect      pawchive.st
// @connect      *.pawchive.st
// ==/UserScript==

/* global fflate, jQuery */

(function waitForLibs() {
    if (typeof fflate === 'undefined' || typeof jQuery === 'undefined') {
        setTimeout(waitForLibs, 100);
        return;
    }

    (function($) {
        'use strict';

        function sanitizeString(str) {
            return str.replace(/[\\/:*?"<>|]/g, '_').trim();
        }

        function showLoadingBar(percent, labelText = '') {
            let bar = $('#kemono-download-loading');
            if (!bar.length) {
                bar = $('<div id="kemono-download-loading">')
                    .css({
                        position: 'fixed', top: '0', left: '0', width: '100%', height: '24px',
                        background: '#1a1a1a', borderBottom: '1px solid #333', zIndex: 99999,
                        color: '#fff', fontSize: '12px', textAlign: 'center', lineHeight: '24px',
                        fontFamily: 'sans-serif'
                    })
                    .append($('<div id="kemono-download-progress">').css({
                        position: 'absolute', top: '0', left: '0', height: '100%',
                        width: '0%', background: 'rgba(255, 149, 0, 0.3)', zIndex: -1
                    }))
                    .append($('<span id="kemono-download-text">'));
                $('body').append(bar);
            }
            $('#kemono-download-progress').css('width', percent + '%');
            $('#kemono-download-text').text(labelText || `Processing: ${percent}%`);
        }

        function hideLoadingBar() {
            $('#kemono-download-loading').remove();
        }

        function secureFetch(url) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'arraybuffer',
                    headers: {
                        'Referer': window.location.origin,
                        'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8'
                    },
                    onload: (res) => {
                        if (res.status >= 200 && res.status < 300) {
                            resolve(new Uint8Array(res.response));
                        } else {
                            reject(new Error(`HTTP ${res.status}`));
                        }
                    },
                    onerror: () => reject(new Error('Network Error'))
                });
            });
        }

        async function downloadKemonoMedia() {
            let mediaLinks = [];

            $('.post__files a.fileThumb, .post__thumbnail a.fileThumb').each(function() {
                const url = $(this).attr('href');
                if (url) {
                    const filename = $(this).attr('download') || url.split('/').pop().split('?')[0];
                    mediaLinks.push({ url, filename });
                }
            });

            $('.post__video, video').each(function() {
                const url = $(this).attr('src') || $(this).find('source').first().attr('src');
                if (url) {
                    const filename = url.split('/').pop().split('?')[0];
                    mediaLinks.push({ url, filename });
                }
            });

            mediaLinks = mediaLinks.filter((media, idx, arr) =>
                arr.findIndex(m => m.filename === media.filename) === idx
            );

            const total = mediaLinks.length;
            if (total === 0) {
                alert('No media links found.');
                return;
            }

            const title = sanitizeString($('.post__title span').first().text() || 'Post');
            const creator = sanitizeString($('.post__user-name').first().text() || 'Artist');
            const zipName = `${title} by ${creator}.zip`;

            showLoadingBar(0, `Preparing workspace for ${total} items...`);

            const zipStructure = {};

            for (let i = 0; i < total; i++) {
                const media = mediaLinks[i];
                const currentIndex = i + 1;
                showLoadingBar(Math.round((i / total) * 100), `Fetching item ${currentIndex} of ${total}...`);

                try {
                    const fileData = await secureFetch(media.url);
                    const ext = media.filename.includes('.') ? media.filename.split('.').pop() : 'png';
                    const customFilename = `${title}_${currentIndex}.${ext}`;

                    zipStructure[customFilename] = fileData;
                } catch (err) {
                    console.error(`Skipped entry map generation for: ${media.filename}`, err);
                }
            }

            showLoadingBar(99, 'Zipping files instantly...');

            fflate.zip(zipStructure, { level: 0 }, (err, data) => {
                hideLoadingBar();

                if (err) {
                    console.error('fflate processing error:', err);
                    alert('Failed to compile zip archive contents.');
                    return;
                }

                const blob = new Blob([data], { type: 'application/zip' });
                const blobUrl = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = blobUrl;
                a.download = zipName;
                document.body.appendChild(a);
                a.click();

                setTimeout(() => {
                    document.body.removeChild(a);
                    window.URL.revokeObjectURL(blobUrl);
                }, 200);
            });
        }

        function insertDownloadButton() {
            if ($('#kemono-download-btn').length) return;
            const actions = $('.post__actions');
            if (!actions.length) return;

            const btn = $('<button id="kemono-download-btn">')
                .text('Download & ZIP')
                .css({
                    marginLeft: '10px', padding: '5px 12px', background: '#ff9500',
                    color: '#fff', border: 'none', borderRadius: '5px',
                    cursor: 'pointer', fontWeight: 'bold', fontSize: '13px'
                })
                .click(function() {
                    $(this).prop('disabled', true).text('Packing...');
                    downloadKemonoMedia().finally(() => {
                        $(this).prop('disabled', false).text('Download & ZIP');
                    });
                });
            actions.append(btn);
        }

        $(document).ready(insertDownloadButton);
        const observer = new MutationObserver(insertDownloadButton);
        observer.observe(document.body, { childList: true, subtree: true });

    })(jQuery);
})();