您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Заменяет кнопку "Буду смотреть" и блок подписки на компактные кнопки для зеркал (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); }); })();