Kinopoisk Free Mirror

Заменяет кнопку "Буду смотреть" и блок подписки на компактные кнопки для зеркал (Kinopoisk.Film, VK, FreeFKSee, ReYoHoHo, GG, Kinopoisk.One, Kinopoisk.CX, Kinopoisk.Gold, Kinopoisk.VIP) и добавляет кнопку "Tape Operator". Удаляет дублирующиеся кнопки и указанные пункты меню из верхнего меню. Работает как на десктопе, так и на мобильных устройствах.

// ==UserScript==
// @name         Kinopoisk Free Mirror
// @namespace    kp-free-mirror-direct
// @version      1.9.10 // <-- Обновлена версия
// @description  Заменяет кнопку "Буду смотреть" и блок подписки на компактные кнопки для зеркал (Kinopoisk.Film, VK, FreeFKSee, ReYoHoHo, GG, Kinopoisk.One, Kinopoisk.CX, Kinopoisk.Gold, Kinopoisk.VIP) и добавляет кнопку "Tape Operator". Удаляет дублирующиеся кнопки и указанные пункты меню из верхнего меню. Работает как на десктопе, так и на мобильных устройствах.
// @author       vaver
// @match        https://www.kinopoisk.ru/film/*
// @match        https://www.kinopoisk.ru/series/*
// @grant        GM_openInTab
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @run-at       document-body // Запуск на ранней стадии
// ==/UserScript==

(function () {
    'use strict';

    const BTN_CLASS = 'kp-free-btn';
    const PLAYER_URL = 'https://tapeop.dev/'; // URL плеера Tape Operator

    // --- Определение типов действий для кнопок ---
    const ACTIONS = {
        CURRENT_TAB: 'currentTab', // Открыть в текущей вкладке
        NEW_TAB: 'newTab',        // Открыть в новой вкладке
        TAPEOP: 'tapeop'          // Специальное действие для Tape Operator
    };

    const WATCH_SOURCES = [
        {
            name: 'Kinopoisk.Film',
            getUrl: (kpId, title) => `https://www.kinopoisk.film/${getCurrentPageType()}/${kpId}/`,
            color: '#00b3ff', // Синий
            action: ACTIONS.CURRENT_TAB
        },
        {
            name: 'ReYoHoHo',
            getUrl: (kpId, title) => `https://reyohoho.github.io/reyohoho/#/movie/${kpId}`,
            color: '#4CAF50', // Зеленый
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'Rutube',
            getUrl: (kpId, title) => {
                const searchQuery = encodeURIComponent(title || ""); // Обработка null
                return `https://rutube.ru/search/?query=${searchQuery}`;
            },
            color: '#DE002B', // Фирменный красный Rutube
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'FreeFKSee',
            getUrl: (kpId, title) => `https://alpha.freefksee.ru/watch/id_${kpId}`, // Исправлены пробелы
            color: '#FF9800', // Оранжевый
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'GG',
            getUrl: (kpId, title) => {
                const transliteratedTitle = transliterate(title || "").replace(/\s+/g, '-').toLowerCase(); // Обработка null
                return `https://www.kinopoisk.gg/${getCurrentPageType()}/${kpId}-${transliteratedTitle}`; // Добавлено название в URL для GG, как часто требуется
            },
            color: '#9C27B0', // Фиолетовый
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'Kinopoisk.One',
            getUrl: (kpId, title) => `https://www.kinopoisk.one/${getCurrentPageType()}/${kpId}/`,
            color: '#FF5722', // Deep Orange
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'Kinopoisk.CX',
            getUrl: (kpId, title) => `https://www.kinopoisk.cx/${getCurrentPageType()}/${kpId}/`,
            color: '#03DAC6', // Teal
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'Kinopoisk.Gold',
            getUrl: (kpId, title) => `https://kinopoisk.gold/${getCurrentPageType()}/${kpId}/`,
            color: '#FFD700', // Gold
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'Kinopoisk.VIP',
            getUrl: (kpId, title) => `https://kinopoisk.vip/${getCurrentPageType()}/${kpId}/`,
            color: '#E91E63', // Pink
            action: ACTIONS.NEW_TAB
        },
        {
            name: 'Tape Operator',
            // getUrl не используется для этой кнопки
            color: '#8a2be2', // Фиолетовый
            action: ACTIONS.TAPEOP // Специальное действие
        }
    ];

    // --- Функция транслитерации (упрощенная) ---
    function transliterate(text) {
        const rus = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
        const eng = "abvgdeezzijklmnoprstufhzcss_y_euaABVGDEEZZIJKLMNOPRSTUFHZCSS_Y_EUA";
        let result = '';
        for (let i = 0; i < text.length; i++) {
            const char = text[i];
            const index = rus.indexOf(char);
            if (index !== -1) {
                result += eng[index];
            } else {
                result += char;
            }
        }
        return result;
    }

    /* ---------- Получить тип страницы (film или series) ---------- */
    function getCurrentPageType() {
        const url = window.location.href;
        if (url.includes('/series/')) {
            return 'series';
        }
        return 'film';
    }

    /* ---------- Извлечение ID фильма/сериала из URL ---------- */
    function getId() {
        try {
            const urlParts = window.location.pathname.split('/');
            const idIndex = urlParts.findIndex(part => part === 'film' || part === 'series') + 1;
            const id = urlParts[idIndex];
            if (id && /^\d+$/.test(id)) {
                console.log("Kinopoisk Free Mirror: Найден ID:", id); // Лог для отладки
                return id;
            }
        } catch (e) {
            console.warn('Kinopoisk Free Mirror: Не удалось извлечь ID из URL.', e);
        }
        console.warn('Kinopoisk Free Mirror: ID не найден в URL.');
        return null;
    }

    /* ---------- Извлечение названия фильма/сериала ---------- */
    function getTitle() {
        let t = null;
        // Пробуем сначала стандартный meta-тег
        let meta = document.querySelector('meta[property="og:title"]');
        if (meta) {
            t = meta.content.trim();
            t = t.replace(/^Кинопоиск\.\s*/i, '').replace(/\s*\(\d{4}\)/, '').trim();
            if (t) {
                console.log("Kinopoisk Free Mirror: Название найдено через meta:", t); // Лог для отладки
                return t;
            }
        }

        // Если meta не помог, пробуем найти заголовок H1 - расширены селекторы
        const titleSelectors = [
            'h1[itemprop="name"]',
            '[class*="styles_title__"]', // Оригинальный селектор
            '[data-tid="e4970878"]', // Пример селектора из логов, может меняться
            '.film-header__title', // Другой возможный селектор
            '.styles_primaryTitle__2b08z', // Ещё один возможный селектор
            'h1' // Очень общий, как крайняя мера
        ];

        for (const selector of titleSelectors) {
            const titleElement = document.querySelector(selector);
            if (titleElement) {
                t = titleElement.textContent.trim();
                t = t.replace(/\s*\(\d{4}\)/, '').trim(); // Убираем год в скобках из любого места
                if (t) {
                    console.log(`Kinopoisk Free Mirror: Название найдено через селектор "${selector}":`, t); // Лог для отладки
                    return t;
                }
            }
        }

        console.warn('Kinopoisk Free Mirror: Не удалось найти название фильма/сериала.');
        return null;
    }

    /* ---------- Создание одной кнопки (компактная версия) ---------- */
    function createButton(source, kpId, title) {
        const btn = document.createElement('button');
        btn.className = BTN_CLASS;
        // Если title не найден, показываем имя источника
        btn.textContent = source.action === ACTIONS.TAPEOP ? source.name : (title ? `Смотреть на ${source.name}` : `${source.name}`);
        // Если title не найден, показываем предупреждение в тултипе
        btn.title = title ? `Открыть "${title}" на ${source.name}` : `Открыть на ${source.name} (Название не найдено)`;

        // Компактный дизайн
        btn.style.cssText = `
            display: inline-flex;
            align-items: center;
            justify-content: center;
            margin: 0 2px;
            padding: 4px 8px;
            border: none;
            border-radius: 4px;
            background: ${source.color};
            color: #fff;
            font-size: 12px;
            font-weight: 500;
            cursor: pointer;
            transition: background 0.2s ease-in-out;
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
            min-width: 80px;
            height: 24px;
            flex-shrink: 0;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        `;

        btn.onmouseover = () => {
            let darkerColor = source.color; // По умолчанию
            // Используем объект для маппинга цветов
            const colorMap = {
                '#00b3ff': '#0090cc',
                '#4CAF50': '#388E3C',
                '#FF9800': '#F57C00',
                '#9C27B0': '#7B1FA2',
                '#4a76a8': '#3a5a80',
                '#8a2be2': '#6a1bb2',
                '#FF5722': '#E64A19',
                '#03DAC6': '#018786',
                '#FFD700': '#FFC107',
                '#E91E63': '#C2185B',
                '#DE002B': '#B00022' // Добавлен для Rutube
            };
            darkerColor = colorMap[source.color] || source.color;
            btn.style.background = darkerColor;
        };
        btn.onmouseout = () => {
            btn.style.background = source.color;
        };

        btn.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation(); // Останавливаем всплытие события
            try { // Добавлен try...catch
                if (source.action === ACTIONS.TAPEOP) {
                    openTapeOpPlayer(kpId, title);
                } else {
                    // Проверка на null kpId для источников, требующих его
                    if (!kpId && source.name !== 'Rutube') { // Rutube может обойтись без ID, используя title
                         alert(`Не удалось получить ID для ${source.name}.`);
                         console.error(`Kinopoisk Free Mirror: Невозможно открыть ${source.name} - нет ID.`);
                         return;
                    }
                    const url = source.getUrl(kpId, title);
                    console.log(`Kinopoisk Free Mirror: Открытие URL для ${source.name}:`, url); // Лог для отладки
                    if (source.action === ACTIONS.CURRENT_TAB) {
                        window.location.href = url;
                    } else if (source.action === ACTIONS.NEW_TAB) {
                        GM_openInTab(url, { active: true });
                    }
                }
            } catch (err) {
                console.error(`Kinopoisk Free Mirror: Ошибка при клике на кнопку ${source.name}:`, err);
                alert(`Произошла ошибка при открытии ${source.name}.`);
            }
        };

        return btn;
    }

    /* ---------- Открытие плеера Tape Operator ---------- */
    async function openTapeOpPlayer(kpId, title) {
        if (!kpId || !title) {
            console.error('Kinopoisk Free Mirror: Невозможно открыть Tape Operator - нет ID или названия.');
            alert('Не удалось получить данные для открытия в Tape Operator.');
            return;
        }

        try {
            const movieData = {
                kinopoisk: kpId,
                title: title
            };

            await GM.setValue('movie-data', movieData);
            console.log('Kinopoisk Free Mirror: Данные для Tape Operator сохранены:', movieData);

            GM_openInTab(PLAYER_URL, { active: true });

        } catch (error) {
            console.error('Kinopoisk Free Mirror: Ошибка при открытии Tape Operator:', error);
            alert('Произошла ошибка при попытке открыть Tape Operator.');
        }
    }

    /* ---------- Удаление дублирующих кнопок и указанных пунктов меню из верхнего меню ---------- */
    function removeTopElements() {
        // Удаляем дублирующиеся кнопки просмотра
        const topWatchButtons = document.querySelectorAll('.kinopoisk-watch-online-button');
        topWatchButtons.forEach(btn => {
            btn.remove();
            console.log('Kinopoisk Free Mirror: Удалена дублирующаяся кнопка из верхнего меню.');
        });

        // Удаляем пункты меню "Билеты в кино" и "Онлайн-кинотеатр"
        const menuItems = document.querySelectorAll('a.kinopoisk-header-featured-menu__item');
        menuItems.forEach(item => {
            const text = item.textContent?.trim(); // Добавлена проверка на null
            if (text && (text.includes('Билеты в кино') || text.includes('Онлайн-кинотеатр'))) {
                item.remove();
                console.log(`Kinopoisk Free Mirror: Удален пункт меню "${text}".`);
            }
        });

        // Удаляем кнопку "Попробовать Плюс"
        const tryPlusButtons = document.querySelectorAll('button');
        tryPlusButtons.forEach(btn => {
            if (btn.textContent && btn.textContent.trim().includes('Попробовать Плюс')) {
                btn.remove();
                console.log('Kinopoisk Free Mirror: Удалена кнопка "Попробовать Плюс".');
            }
        });

        // Удаляем блок "Подписка Яндекс Плюс" (старая попытка)
        const subscriptionBlocks = document.querySelectorAll('.styles_subscriptionText__YOJhf');
        subscriptionBlocks.forEach(block => {
            const parent = block.closest('.styles_watchingServicesOnline__9_agN');
            if (parent) {
                parent.remove();
                console.log('Kinopoisk Free Mirror: Удален блок "Подписка Яндекс Плюс" (старый способ).');
            }
        });
    }

    /* ---------- Основная логика замены кнопки "Буду смотреть" и блока подписки ---------- */
    function replaceWillWatchButton() {
        const kpId = getId();
        const title = getTitle();

        console.log("Kinopoisk Free Mirror: replaceWillWatchButton вызван. ID:", kpId, "Title:", title); // Лог для отладки

        if (!kpId) {
            console.warn('Kinopoisk Free Mirror: Не удалось получить ID.');
            return;
        }
        // title может быть null, кнопки всё равно создаются, но с предупреждением.

        // Удаляем ненужные элементы из верхнего меню
        removeTopElements();

        // --- Определение: мобильное устройство? ---
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

        let targetContainer; // Для кнопки "Буду смотреть"
        let subscriptionContainer; // Для блока подписки

        // --- НОВАЯ ЛОГИКА ПОИСКА ---
        // Ищем контейнер кнопок, как в вашем примере
        const potentialButtonContainers = document.querySelectorAll('div[class*="styles_buttonsContainer__"]');
        console.log("Kinopoisk Free Mirror: Найдено потенциальных контейнеров кнопок:", potentialButtonContainers.length);

        if (potentialButtonContainers.length > 0) {
            // Предполагаем, что первый найденный - нужный
            targetContainer = potentialButtonContainers[0];
            console.log("Kinopoisk Free Mirror: Найден основной контейнер кнопок:", targetContainer);
        } else {
            // Старая логика поиска как запасной вариант
            if (isMobile) {
                targetContainer = document.querySelector('.styles_countriesAndDuration__RQTd9');
                subscriptionContainer = document.querySelector('.styles_subscriptionOffer__eau7V');
            } else {
                targetContainer = document.querySelector('.styles_buttonsContainer__Kcrch');
                subscriptionContainer = document.querySelector('.styles_subscriptionOffer__eau7V');
            }
        }

        // Проверяем, не были ли кнопки уже добавлены (проверяем по уникальному классу контейнера)
        if (document.querySelector('.kp-free-button-container')) {
            console.log('Kinopoisk Free Mirror: Кнопки уже добавлены.');
            return; // Кнопки уже есть
        }

        // Создаем контейнер с новыми кнопками
        const newButtonContainer = document.createElement('div');
        newButtonContainer.className = 'kp-free-button-container';
        newButtonContainer.style.cssText = `
            display: flex;
            flex-wrap: wrap;
            gap: 4px;
            width: 100%;
            margin-top: 10px;
            margin-bottom: 10px;
            justify-content: flex-start;
        `;

        // Создаем кнопки, даже если title null (кроме случаев, где он критичен)
        WATCH_SOURCES.forEach(source => {
            // Для источников, кроме Rutube, kpId критичен. Для Rutube можно попробовать без ID.
            if (kpId || source.name === 'Rutube') {
                const btn = createButton(source, kpId, title);
                newButtonContainer.appendChild(btn);
            }
        });

        let containerInserted = false;

        // --- НОВАЯ ЛОГИКА ЗАМЕНЫ ---
        if (targetContainer && targetContainer.parentNode) {
            // Очищаем оригинальный контейнер
            targetContainer.innerHTML = '';
            // Добавляем наши кнопки в него
            targetContainer.appendChild(newButtonContainer);
            containerInserted = true;
            console.log('Kinopoisk Free Mirror: Блок кнопок "Буду смотреть" заменен на компактные кнопки для зеркал (внутри оригинального контейнера).');
        }

        // --- СТАРАЯ ЛОГИКА ЗАМЕНЫ/ВСТАВКИ как запасной вариант ---
        if (!containerInserted) {
             // Пытаемся заменить оригинальный контейнер кнопок "Буду смотреть"
             // Используем insertAdjacentElement вместо replaceChild для большей надежности
             if (targetContainer) {
                 targetContainer.insertAdjacentElement('afterend', newButtonContainer.cloneNode(true));
                 // targetContainer.style.display = 'none'; // Вместо удаления, просто скрываем
                 containerInserted = true;
                 console.log('Kinopoisk Free Mirror: Блок кнопок "Буду смотреть" заменен на компактные кнопки для зеркал (inserted after).');
             }

             // Пытаемся удалить блок подписки и вставить кнопки туда
             if (subscriptionContainer && subscriptionContainer.parentNode) {
                 // Вставляем кнопки ПЕРЕД блоком подписки
                 subscriptionContainer.parentNode.insertBefore(newButtonContainer, subscriptionContainer);
                 // Удаляем блок подписки
                 subscriptionContainer.remove();
                 containerInserted = true;
                 console.log('Kinopoisk Free Mirror: Блок подписки удален, кнопки вставлены.');
             }
        }

        // Если ни один из контейнеров не был найден и заменен/вставлен, вставляем в общий контейнер
        if (!containerInserted) {
             console.warn('Kinopoisk Free Mirror: Не найдены стандартные контейнеры для замены или вставки кнопок. Попытка вставки в общий контейнер.');
             // Попробуем найти более общий контейнер, если специфичные не работают
             const generalContainer = document.querySelector('[data-tid="7e7815d3"], [data-tid="film-page-root"], .film-page-content, .styles_root__') || document.body;
             if (generalContainer) {
                 // Вставляем в начало общего контейнера или в конец body
                 generalContainer.insertAdjacentElement('afterbegin', newButtonContainer);
                 containerInserted = true;
                 console.log('Kinopoisk Free Mirror: Кнопки вставлены в общий контейнер.');
             } else {
                 console.error('Kinopoisk Free Mirror: Не найден даже общий контейнер для вставки кнопок.');
             }
        }

        if (containerInserted) {
            console.log('Kinopoisk Free Mirror: Кнопки успешно добавлены на страницу.');
        }
    }


    /* ---------- Инициализация с ожиданием загрузки кнопки ---------- */
    function initialize() {
        console.log("Kinopoisk Free Mirror: Инициализация скрипта...");
        // Пробуем заменить кнопку сразу
        replaceWillWatchButton();

        // Если кнопка еще не загрузилась, ждем с помощью MutationObserver
        // Наблюдаем за изменениями в body
        const observer = new MutationObserver((mutations, obs) => {
            // Проверяем, появились ли нужные элементы
            if (document.querySelector('div[class*="styles_buttonsContainer__"]') ||
                document.querySelector('.styles_buttonsContainer__Kcrch') ||
                document.querySelector('.styles_countriesAndDuration__RQTd9') ||
                document.querySelector('.styles_subscriptionOffer__eau7V') ||
                document.querySelector('[data-tid="7e7815d3"]')) {
                console.log("Kinopoisk Free Mirror: MutationObserver обнаружил потенциальные изменения.");
                replaceWillWatchButton();
                // Можно отключить observer после первой успешной вставки, если это необходимо
                // obs.disconnect();
            }
        });

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

        // Дополнительная проверка через 2 и 5 секунд
        setTimeout(() => {
            console.log("Kinopoisk Free Mirror: Повторная попытка через 2 секунды.");
            replaceWillWatchButton();
        }, 2000);
        setTimeout(() => {
            console.log("Kinopoisk Free Mirror: Повторная попытка через 5 секунд.");
            replaceWillWatchButton();
        }, 5000);

        // Очень долгая проверка, если всё остальное не помогло
        setTimeout(() => {
            console.log("Kinopoisk Free Mirror: Финальная попытка через 10 секунд.");
            replaceWillWatchButton();
        }, 10000);
    }

    // Запуск после полной загрузки DOM
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            console.log("Kinopoisk Free Mirror: DOMContentLoaded событие.");
            initialize();
        });
    } else {
        console.log("Kinopoisk Free Mirror: DOM уже загружен, запуск initialize.");
        initialize();
    }

    // Альтернативный запуск на window.load для полной уверенности
    window.addEventListener('load', () => {
         console.log("Kinopoisk Free Mirror: window.load событие.");
         // Повторный запуск, если по какой-то причине не сработало
         setTimeout(replaceWillWatchButton, 100);
    });

})();