Kemono FIX+Download

Embeds a "Download" button before each file element and starts downloading and saving it to your computer, can use constant to change replace kemono image server if standart not work.

Fra 18.12.2024. Se den seneste versjonen.

// ==UserScript==
// @name         Kemono FIX+Download
// @namespace    GPT
// @version      1.1.2
// @description  Embeds a "Download" button before each file element and starts downloading and saving it to your computer, can use constant to change replace kemono image server if standart not work.
// @description:ru  Встраивает кнопку Download перед каждым элементом с файлами и запускает скачивание с сохранением на компьютер, так же имеет константу для смены сервера изображений если стандартный не работает.
// @author       Wizzergod
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kemono.su
// @homepageURL  https://greasyfork.org/ru/users/712283-wizzergod
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_info
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM.xmlHttpRequest
// @grant        GM_getResourceUrl
// @grant        GM.openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @grant        GM_notification
// @match        *://kemono.su/*
// @match        *://kemono.party/*
// @match        *://coomer.su/*
// @match        *://*.patreon.com/*
// @match        *://*.fanbox.cc/*
// @match        *://*.pixiv.net/*
// @match        *://*.discord.com/*
// @match        *://*.fantia.jp/*
// @match        *://*.boosty.to/*
// @match        *://*.dlsite.com/*
// @match        *://*.gumroad.com/*
// @match        *://*.subscribestar.com/*
// @match        *://*.subscribestar.adult/*
// @match        *://*.onlyfans.com/*
// @match        *://*.candfans.jp/*
// @connect      kemono.party
// @connect      kemono.su
// @connect      coomer.su
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // Константы для включения/выключения функций
    let ENABLE_IMAGE_REPLACEMENT = 0;  // 1 - включено, 0 - выключено
    let ENABLE_URL_REPLACEMENT = 0;   // 1 - включено, 0 - выключено
    const ENABLE_MUTATION_OBSERVER = 1; // 1 - включено, 0 - выключено

    // Дебаунс функция для оптимизации MutationObserver
    let debounceTimeout;
    const debounce = (callback, delay = 300) => {
        clearTimeout(debounceTimeout);
        debounceTimeout = setTimeout(callback, delay);
    };

    // Core configuration constants
    const CONSTANTS = {
        API_BASE_URL: "https://kemono.su/api/v1",
        GRID_GAP: "16px",
        GRID_MAX_WIDTH: "1600px",
        GRID_MIN_COLUMN_WIDTH: "250px",
        IMAGE_BASE_URL: "https://img.kemono.su/thumbnail",
        SELECTORS: {
            GRID: ".card-list__items",
            POST_CARD: ".post-card",
            POST_IMAGE: ".post-card__image",
        },
        SUPPORTED_IMAGE_EXTENSIONS: [
            ".bmp",
            ".gif",
            ".jpeg",
            ".jpg",
            ".png",
            ".webp",
        ],
    };

    // Стили для кнопок и контейнеров
    GM_addStyle(`
        .download-container {
            padding: 7px;
            text-align: center;
            border: none;
            width: 100%;
            display: inline-block;
            justify-content: center;
            transform: translate(0%, -110%);
        }

        .download-button {
            padding: 1px 7%;
            background-color: #4CAF50;
            background-position: center;
            background-repeat: no-repeat;
            color: white;
            border: solid rgba(128,128,128,.7) .125em;
            cursor: pointer;
            font-size: 14px;
            display: inline-block;
            min-width: 80%;
            width: 98%;
            text-align: center;
            border-radius: 5px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.57), -3px -3px rgba(0, 0, 0, .1) inset;
            transition: transform 0.3s ease, background-color 0.3s ease;
            justify-content: center;
            font-size: 20px;
            opacity: 0.8;
        }

        .download-button:hover {
            background-color: #4C9CAF;
        }

        .post__thumbnail {
            max-width: 10%;
            min-width: 20%;
            height: auto;
            cursor: pointer;
            padding: 5px;
            border: 5px;
            display: inline-flex;
            flex-direction: column;
            flex-wrap: wrap;
            align-content: center;
            justify-content: space-between;
            align-items: center;
            margin-top: -3.5%;
        }

        .post__thumbnail img:hover {
            transform: scale(1.1);
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            transition: transform 0.3s ease, background-color 0.3s ease;
        }

        .post__thumbnail img {
            cursor: pointer;
            border-radius: 5px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            transition: transform 0.3s ease, background-color 0.3s ease;
            border: solid rgba(128,128,128,.7) .125em;
        }

        .post-card__image-container {
            max-width: 100%;
            height: auto;
        }

        .post__files {
            display: flex;
            padding: 50px;
            border: 5px;
            max-width: 100%;
            min-width: 70%;
            flex-direction: row;
            flex-wrap: wrap;
            align-content: center;
            justify-content: center;
            align-items: center;
        }

        [data-testid='tracklist-row'] .newButtonClass {
            position: absolute;
            top: 50%;
            right: calc(100% + 10px);
            transform: translateY(-50%);
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
        }

       .post__content p img, .post__content h2 img, .post__content h3 img {
            width: 20%;
            cursor: pointer;
            border: solid rgba(128,128,128,.7) .125em;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            margin: 5px auto;
            border-radius: 10px;
            transition: transform 0.3s ease, background-color 0.3s ease;
        }
       .post__content p img:hover, .post__content h2 img:hover, .post__content h3 img:hover {
            transform: translate(50%) scale(1.5); /* используем translate для точного центрирования */
            z-index: 9999; /* картинка будет выше всех других элементов */
       }

       .post__content p {
         padding: 1px 7%;
         margin: 5px;
        }

        /* Чтобы иконка не перекрывала текст ссылки */
        a.post__attachment-link {
            position: relative;
        }

        .post__nav-links {
            position: sticky; /* Задает прилипание элемента */
            top: 0; /* Фиксирует элемент сверху на странице */
            z-index: 100; /* Устанавливает уровень слоя для избегания перекрытия другими элементами */
            background-color: #000000ba; /* Опционально, чтобы фоновый цвет скрывал элементы позади */
            border: solid rgba(128,128,128,.7) .125em;
            border-radius: 10px; /*  border-radius: 10px 10px 0 0; */
            width:99%;
            top: 5px; /* Учитывает отступ сверху */
            margin: 5px auto; /* Вертикальный отступ 5px и центрирование по горизонтали */
        }

    `);

    // Функция для получения имени файла из URL (используя параметр f, если он есть)
    const getFileName = (url) => {
        return new URLSearchParams(new URL(url).search).get('f') || url.split('/').pop().split('?')[0];
    };

    // Функция для скачивания файла
    const downloadFile = (url) => {
        const fileName = getFileName(url);
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob',
            onload: (response) => {
                const link = document.createElement('a');
                link.href = URL.createObjectURL(response.response);
                link.download = fileName;
                link.click();
                URL.revokeObjectURL(link.href);
            },
            onerror: (error) => {
                console.error('Ошибка загрузки файла:', error);
            },
        });
    };

    // Функция для асинхронной загрузки изображения с оригинальным именем
    const downloadImage = async (url) => {
        try {
            const fileName = getFileName(url);  // Получаем имя файла
            const response = await fetch(url);
            const blob = await response.blob();
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = fileName; // Используем имя файла из URL или параметра f
            link.click();
            URL.revokeObjectURL(link.href);
        } catch (error) {
            console.error('Ошибка загрузки файла:', error);
        }
    };

    // Функция для создания кнопки Download
    const createDownloadButton = (onClickCallback) => {
        const button = document.createElement('button');
        button.textContent = 'Download';
        button.className = 'download-button';
        button.addEventListener('click', onClickCallback);
        return button;
    };

    // Добавление кнопки к миниатюрам
    const addDownloadButtonsToThumbnails = () => {
        document.querySelectorAll('.post__thumbnail').forEach((thumbnail) => {
            const link = thumbnail.querySelector('a.fileThumb');
            if (!link || thumbnail.querySelector('.download-container')) return;

            const downloadContainer = document.createElement('div');
            downloadContainer.className = 'download-container';
            const button = createDownloadButton((event) => {
                event.preventDefault();
                downloadFile(link.href);
            });

            downloadContainer.appendChild(button);
            thumbnail.appendChild(downloadContainer);
        });
    };

    // Добавление кнопок сразу при загрузке
    addDownloadButtonsToThumbnails();

    // Отслеживание изменений в DOM
    const observer = new MutationObserver(() => {
        addDownloadButtonsToThumbnails();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
    });

    // Функция для замены атрибутов src и data-src изображений
    function replaceImageSources() {
        const imageLinks = document.querySelectorAll('.post__files .post__thumbnail a.fileThumb.image-link');
        imageLinks.forEach((link) => {
            const imgElement = link.querySelector('img');
            if (imgElement) {
                const originalSrc = link.href;
                const dataSrc = imgElement.getAttribute('data-src');
                if (ENABLE_IMAGE_REPLACEMENT === 1) {
                    imgElement.setAttribute('src', originalSrc);
                    imgElement.setAttribute('data-src', originalSrc);
                }
            }
        });
    }

    // Функция для замены URL для миниатюр
    function replaceThumbnailURLs() {
        const imageLinks = document.querySelectorAll('.post__files .post__thumbnail a.fileThumb.image-link, .post-card__image-container');
        imageLinks.forEach((link) => {
            const imgElement = link.querySelector('img');
            if (imgElement) {
                let src = imgElement.getAttribute('src');
                let dataSrc = imgElement.getAttribute('data-src');
                if (ENABLE_URL_REPLACEMENT === 1) {
                    if (src && src.includes('img.kemono.su/thumbnail/data/')) {
                        src = src.replace('img.kemono.su/thumbnail/data/', 'n3.kemono.su/data/');
                        imgElement.setAttribute('src', src);
                    }
                    if (dataSrc && dataSrc.includes('img.kemono.su/thumbnail/data/')) {
                        dataSrc = dataSrc.replace('img.kemono.su/thumbnail/data/', 'n3.kemono.su/data/');
                        imgElement.setAttribute('data-src', dataSrc);
                    }
                }
            }
        });
    }

    // Регистрируем команды меню для включения/выключения функций
    GM_registerMenuCommand('Включить замену изображений', () => {
        ENABLE_IMAGE_REPLACEMENT = 1;
        replaceImageSources();
    }, 'r');

    GM_registerMenuCommand('Отключить замену изображений', () => {
        ENABLE_IMAGE_REPLACEMENT = 0;
    }, 'r');

    // Проверка и добавление кнопок
    addDownloadButtonsToThumbnails();

    // Используем MutationObserver с дебаунсом для отслеживания изменений в DOM
    if (ENABLE_MUTATION_OBSERVER) {
        const observer = new MutationObserver(() => {
            debounce(() => {
                if (ENABLE_IMAGE_REPLACEMENT === 1) replaceImageSources();
                if (ENABLE_URL_REPLACEMENT === 1) replaceThumbnailURLs();
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Если включены замены, выполняем их сразу после загрузки
    if (ENABLE_IMAGE_REPLACEMENT === 1) {
        replaceImageSources();
    }

    if (ENABLE_URL_REPLACEMENT === 1) {
        replaceThumbnailURLs();
    }

})();