Kemono File Ripper

Rips all images and videos from Kemono and downloads them as a zip

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Kemono File Ripper
// @namespace    Tampermonkey/Violentmonkey Scripts
// @version      1.2.1
// @description  Rips all images and videos from Kemono and downloads them as a zip
// @author       Selentia
// @icon         https://raw.githubusercontent.com/Selentia-IX/kemono-file-ripper/main/kr-png.png
// @match        https://kemono.party/*
// @match        https://kemono.cr/*
// @copyright    2025+, Selentia (https://github.com/Selentia-IX/kemono-file-ripper)
// @license      GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @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
// @grant        none
// @description  Rips all the static images and videos and files from Kemono and downloads them as a zip
// ==/UserScript==

/* global JSZip, saveAs, jQuery */

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

    (function($) {
        'use strict';

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

        function showLoadingBar(percent) {
            let bar = $('#kemono-download-loading');
            if (!bar.length) {
                bar = $('<div id="kemono-download-loading">')
                    .css({
                        position: 'fixed',
                        top: '0',
                        left: '0',
                        width: '100%',
                        height: '5px',
                        background: '#eee',
                        zIndex: 99999
                    })
                    .append($('<div id="kemono-download-progress">').css({
                        height: '100%',
                        width: '0%',
                        background: '#ff9500'
                    }));
                $('body').append(bar);
            }
            $('#kemono-download-progress').css('width', percent + '%');
        }

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

        async function downloadKemonoMedia() {
            const zip = new JSZip();
            let mediaLinks = [];

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

            $('.post__video').each(function() {
                const url = $(this).attr('src');
                if (url) {
                    let filename = url.split('/').pop().split('?')[0];
                    const att = $('.post__attachment-link[href*="' + filename + '"]');
                    if (att.length) filename = att.attr('download') || filename;
                    mediaLinks.push({url, filename});
                }
            });

            $('.post__attachment-link').each(function() {
                const url = $(this).attr('href');
                const filename = $(this).attr('download') || url.split('/').pop().split('?')[0];
                if (!mediaLinks.some(m => m.filename === filename)) {
                    mediaLinks.push({url, filename});
                }
            });
            // filters duplicates (feel free to remove it if it conflicts with the file you want)
            mediaLinks = mediaLinks.filter((media, idx, arr) =>
                arr.findIndex(m => m.filename === media.filename) === idx
            );

            const total = mediaLinks.length;
            let completed = 0;

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

            if (total === 0) {
                alert('No images or videos found!');
                return;
            }

            showLoadingBar(0);

            let saveHandler = saveAs;
            let usePicker = false;
            if ('showSaveFilePicker' in window) {
                usePicker = true;
            }

            const fetchPromises = mediaLinks.map(media =>
                fetch(media.url)
                    .then(response => {
                        if (!response.ok) throw new Error('Network error');
                        return response.blob();
                    })
                    .then(blob => {
                        const index = mediaLinks.indexOf(media) + 1;
                        const ext = media.filename.includes('.') ? media.filename.split('.').pop() : '';
                        const baseName = title !== 'Kemono_Post' ? title : 'file';
                        const newFilename = `${sanitizeString(baseName)}_${index}${ext ? '.' + ext : ''}`;
                        zip.file(newFilename,  blob);
                    })
                    .catch(() => {
                        alert(`Failed to download ${media.filename}`);
                    })
                    .finally(() => {
                        completed++;
                        showLoadingBar(Math.round((completed / total) * 100));
                    })
            );

            await Promise.all(fetchPromises);

            zip.generateAsync({type:"blob"}).then(async function(content) {
                hideLoadingBar();
                if (usePicker) {
                    try {
                        const handle = await window.showSaveFilePicker({
                            suggestedName: zipName,
                            types: [{
                                description: 'Zip Files',
                                accept: {'application/zip': ['.zip']}
                            }]
                        });
                        const writable = await handle.createWritable();
                        await writable.write(content);
                        await writable.close();
                    } catch (e) {
                        saveHandler(content, zipName);
                    }
                } else {
                    saveHandler(content, zipName);
                }
            });
        }

        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 Media')
                .css({
                    marginLeft: '10px',
                    padding: '5px 10px',
                    background: '#ff9500',
                    color: '#fff',
                    border: 'none',
                    borderRadius: '5px',
                    cursor: 'pointer'
                })
                .click(function() {
                    $(this).prop('disabled', true);
                    downloadKemonoMedia().finally(() => {
                        $(this).prop('disabled', false);
                    });
                });
            actions.append(btn);
        }

        $(document).ready(function() {
            insertDownloadButton();
        });

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