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.

Ajankohdalta 4.12.2024. Katso uusin versio.

// ==UserScript==
// @name         Kemono FIX+Download
// @namespace    GPT
// @version      1.0.9
// @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
// @match        *://kemono.su/*
// @match        *://kemono.party/*
// @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);
    };

    // Добавляем стиль для кнопки Download и контейнера
    GM_addStyle(`
        .download-container {
            margin-top: 2px;
            padding: 5px;
            background-position: center;
            background-repeat: no-repeat;
            text-align: center;
            border: none;
            width: 100%;
            display: inline-block;
            transition: transform 0.3s ease, background-color 0.3s ease;
            justify-content: center;
            font-size: 30px;
            opacity: 0.9;
            transform: translate(0%, -110%);
        }

        .download-button {
            padding: 1px 7%;
            background-color: #4CAF50;
            background-position: center;
            background-repeat: no-repeat;
            color: white;
            border: none;
            cursor: pointer;
            font-size: 14px;
            display: inline-block;
            min-width: 100px;
            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;
}

.post-card__image-container {
	max-width: 100%;
	height: auto;
}
.post__files {
    display: flex;
    padding: 5px;
    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);
    }
    `);


    // Функция для асинхронной загрузки изображения
    const downloadImage = async (url) => {
        try {
            const response = await fetch(url);
            const blob = await response.blob();
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = url.split('/').pop().split('?')[0];
            link.click();
            URL.revokeObjectURL(link.href);
        } catch (error) {
            console.error('Ошибка загрузки файла:', error);
        }
    };

    // Функция для замены атрибутов 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);
                    }
                }
            }
        });
    }

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

    // Функция для добавления кнопок Download
    function addDownloadButtons() {
        try {
            const fileElements = document.querySelectorAll('.post__files .post__thumbnail a.fileThumb.image-link');
            if (!fileElements.length) throw new Error('Элементы не найдены');
            fileElements.forEach((fileElement) => {
                if (fileElement.parentElement.querySelector('.download-container')) {
                    return;
                }
                const downloadContainer = document.createElement('div');
                downloadContainer.classList.add('download-container');
                const downloadButton = createDownloadButton((event) => {
                    event.preventDefault();
                    const fileUrl = fileElement.href;
                    downloadImage(fileUrl);
                });
                downloadContainer.appendChild(downloadButton);
                fileElement.parentElement.appendChild(downloadContainer);
            });
        } catch (error) {
            console.error('Ошибка при добавлении кнопок загрузки:', error);
        }
    }

    // Двойная проверка добавления кнопок с таймаутом
    function checkAndAddButtons() {
        addDownloadButtons();
        setTimeout(() => {
            addDownloadButtons();
        }, 500);
    }

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

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

    // Вызываем проверку и добавление кнопок
    checkAndAddButtons();

    // Используем 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();
    }

})();