🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Kinopoisk Free Mirror: Customizable v5.1

Зеркала, настройка кнопок, отсутствие мерцания и лагов.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         Kinopoisk Free Mirror: Customizable v5.1
// @namespace    kp-free-mirror-direct
// @version      5.1.0
// @description  Зеркала, настройка кнопок, отсутствие мерцания и лагов.
// @author       Evreu1pro
// @match        https://www.kinopoisk.ru/film/*
// @match        https://www.kinopoisk.ru/series/*
// @match        *://tapeop.dev/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kinopoisk.ru
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @run-at       document-body
// ==/UserScript==

(function () {
    'use strict';

    // --- КОНФИГУРАЦИЯ ---
    const CONFIG = {
        telegramUrl: 'https://t.me/gostibissi',
        tapeOpUrl: 'https://tapeop.dev/',
        storageKey: 'kp_mirror_v5_data_local'
    };

    const DEFAULT_SOURCES = [
        { id: 'kp_film', name: 'Kinopoisk.Film', color: '#00b3ff', template: 'https://www.kinopoisk.film/{type}/{id}/', enabled: true },
        { id: 'reyohoho', name: 'ReYoHoHo', color: '#4CAF50', template: 'https://reyohoho.github.io/reyohoho/#/movie/{id}', enabled: true },
        { id: 'kp_gg', name: 'GG', color: '#9C27B0', template: 'https://www.kinopoisk.gg/{type}/{id}-{translit}', enabled: true },
        { id: 'freefk', name: 'FreeFKSee', color: '#FF9800', template: 'https://alpha.freefksee.ru/watch/id_{id}', enabled: true },
        { id: 'kp_one', name: 'Kinopoisk.One', color: '#FF5722', template: 'https://www.kinopoisk.one/{type}/{id}/', enabled: true },
        { id: 'rutube', name: 'Rutube', color: '#DE002B', template: 'https://rutube.ru/search/?query={title}', enabled: true },
        { id: 'vk', name: 'VK Video', color: '#0077FF', template: 'https://vkvideo.ru/?q={title}', enabled: true },
        { id: 'tapeop', name: 'Tape Operator', color: '#6C5CE7', template: 'special:tapeop', enabled: true }
    ];

    // --- CSS СТИЛИ ---
    const STYLES_CSS = `
        /* Основной контейнер - на всю ширину */
        .kp-mirror-wrapper {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 15px;
            margin-bottom: 15px;
            align-items: center;
            font-family: 'Graphik Kinopoisk LC', sans-serif;
            justify-content: center;
            width: 100%;
            clear: both;
            padding: 0 16px; /* Отступы как у основного контента */
            box-sizing: border-box;
        }

        @media (max-width: 768px) {
            .kp-mirror-wrapper {
                gap: 6px;
            }
            .kp-mirror-btn {
                flex-grow: 1;
                min-width: 90px;
                padding: 12px 4px !important;
                font-size: 13px !important;
            }
        }

        .kp-mirror-btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px 14px; border: none; border-radius: 8px; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; text-decoration: none; transition: transform 0.2s ease, filter 0.2s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.2); opacity: 0.95; position: relative; user-select: none; -webkit-tap-highlight-color: transparent; }
        .kp-mirror-btn:hover { opacity: 1; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); filter: brightness(1.1); }

        .kp-telegram-btn { background: #24A1DE; padding: 8px 14px; gap: 6px; }
        .kp-telegram-icon { width: 16px; height: 16px; fill: white; }

        .kp-settings-btn { background: #333; width: 36px; height: 36px; padding: 0; border-radius: 50%; flex-grow: 0 !important; min-width: 36px !important; }
        .kp-settings-icon { width: 20px; height: 20px; fill: #ccc; transition: transform 0.5s ease; }
        .kp-settings-btn:hover .kp-settings-icon { fill: #fff; transform: rotate(90deg); }

        .kp-hotkey-hint { font-size: 9px; position: absolute; top: -4px; right: -4px; background: #333; color: #fff; padding: 1px 4px; border-radius: 4px; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
        .kp-mirror-wrapper:hover .kp-hotkey-hint { opacity: 1; }

        /* Модальное окно */
        .kp-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 9999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; backdrop-filter: blur(3px); }
        .kp-modal-overlay.active { opacity: 1; pointer-events: auto; }
        .kp-modal { background: #1f1f1f; color: #fff; padding: 20px; border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 20px 50px rgba(0,0,0,0.5); z-index: 10000; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
        .kp-modal h3 { margin: 0; font-size: 18px; border-bottom: 1px solid #333; padding-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
        .kp-source-list { display: flex; flex-direction: column; gap: 5px; max-height: 300px; overflow-y: auto; padding-right: 5px; }
        .kp-source-item { display: flex; align-items: center; justify-content: space-between; background: #2a2a2a; padding: 8px 12px; border-radius: 6px; user-select: none; }
        .kp-source-item.disabled { opacity: 0.6; }
        .kp-source-left { display: flex; align-items: center; gap: 10px; flex-grow: 1; }
        .kp-checkbox { accent-color: #f60; cursor: pointer; width: 16px; height: 16px; }
        .kp-source-name { font-weight: 600; }
        .kp-source-actions { display: flex; gap: 5px; }
        .kp-action-btn { background: #444; border: none; color: #fff; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: background 0.2s; }
        .kp-action-btn:hover { background: #666; }
        .kp-action-btn.delete { background: #8B0000; }
        .kp-add-form { background: #252525; padding: 10px; border-radius: 6px; display: flex; flex-direction: column; gap: 8px; border: 1px solid #333; }
        .kp-form-row { display: flex; gap: 8px; }
        .kp-input { background: #111; border: 1px solid #444; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 13px; flex-grow: 1; }
        .kp-input-color { width: 50px; padding: 0; border: none; height: 30px; cursor: pointer; }
        .kp-modal-footer { border-top: 1px solid #333; padding-top: 15px; display: flex; justify-content: space-between; align-items: center; }
        .kp-btn-primary { background: #f60; color: #fff; border: none; padding: 8px 20px; border-radius: 6px; cursor: pointer; font-weight: bold; }
        .kp-btn-close { background: transparent; color: #aaa; border: none; cursor: pointer; text-decoration: underline; }
        .kp-hint { font-size: 11px; color: #888; margin-top: 4px; }
    `;

    function injectStyles(css) {
        const head = document.head || document.getElementsByTagName('head')[0];
        const style = document.createElement('style');
        style.appendChild(document.createTextNode(css));
        head.appendChild(style);
    }
    injectStyles(STYLES_CSS);

    // --- SAFE API ---
    const SafeAPI = {
        save: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.error(e); } },
        load: (key) => { try { const val = localStorage.getItem(key); return val ? JSON.parse(val) : null; } catch (e) { return null; } },
        openTab: (url) => {
            if (typeof GM_openInTab === 'function') GM_openInTab(url, { active: true });
            else if (typeof GM !== 'undefined' && GM.openInTab) GM.openInTab(url, { active: true });
            else window.open(url, '_blank');
        }
    };

    const Utils = {
        transliterate: (text) => {
            if (!text) return '';
            const rus = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
            const eng = "abvgdeezzijklmnoprstufhzcss_y_euaABVGDEEZZIJKLMNOPRSTUFHZCSS_Y_EUA";
            let res = '';
            for (let i = 0; i < text.length; i++) { const idx = rus.indexOf(text[i]); res += idx !== -1 ? eng[idx] : text[i]; }
            return res.replace(/\s+/g, '-').toLowerCase();
        },
        getPageType: () => window.location.href.includes('/series/') ? 'series' : 'film',
        getId: () => {
            const parts = window.location.pathname.split('/');
            const idx = parts.findIndex(p => p === 'film' || p === 'series');
            return (idx !== -1 && parts[idx + 1]) ? parts[idx + 1] : null;
        },
        getTitle: () => {
            let t = document.querySelector('meta[property="og:title"]')?.content;
            if (t) return t.replace(/^Кинопоиск\.\s*/i, '').replace(/\s*\(\d{4}\)/, '').trim();
            t = document.querySelector('h1')?.textContent;
            return t ? t.replace(/\s*\(\d{4}\)/, '').trim() : null;
        }
    };

    class KinopoiskEnhancer {
        constructor() {
            this.data = { forceNewTab: true, sources: [...DEFAULT_SOURCES] };
            this.renderTimeout = null;
            this.init();
        }

        init() {
            this.loadData();
            this.setupHotkeys();
            this.debouncedRender();
            this.startObserver();
        }

        loadData() {
            const stored = SafeAPI.load(CONFIG.storageKey);
            if (stored) {
                this.data.forceNewTab = stored.forceNewTab ?? true;
                if (stored.sources && Array.isArray(stored.sources)) this.data.sources = stored.sources;
            }
        }

        saveData() { SafeAPI.save(CONFIG.storageKey, this.data); }

        buildUrl(template, kpId, title) {
            if (template === 'special:tapeop') return 'special:tapeop';
            let url = template;
            const type = Utils.getPageType();
            const translit = Utils.transliterate(title);
            url = url.replace(/{id}/g, kpId || '').replace(/{type}/g, type).replace(/{title}/g, encodeURIComponent(title || '')).replace(/{translit}/g, translit);
            return url;
        }

        setupHotkeys() {
            document.addEventListener('keydown', (e) => {
                if (e.altKey && e.key >= '1' && e.key <= '9') {
                    const index = parseInt(e.key) - 1;
                    const activeSources = this.data.sources.filter(s => s.enabled);
                    if (activeSources[index]) {
                        const kpId = Utils.getId();
                        const title = Utils.getTitle();
                        this.openSource(activeSources[index], kpId, title);
                    }
                }
            });
        }

        static initTapeOpReceiver() {
            const data = SafeAPI.load('movie-data');
            if (data) {
                localStorage.removeItem('movie-data');
                const script = document.createElement('script');
                script.innerHTML = `globalThis.init(JSON.parse('${JSON.stringify(data)}'), '5.1');`;
                document.body.appendChild(script);
            }
        }

        openSource(source, kpId, title) {
            if (source.template === 'special:tapeop') {
                SafeAPI.save('movie-data', { kinopoisk: kpId, title });
                SafeAPI.openTab(CONFIG.tapeOpUrl);
                return;
            }
            const url = this.buildUrl(source.template, kpId, title);
            if (this.data.forceNewTab) SafeAPI.openTab(url);
            else window.location.href = url;
        }

        createSettingsModal() {
            if (document.getElementById('kp-settings-modal')) return;
            const overlay = document.createElement('div');
            overlay.id = 'kp-settings-modal';
            overlay.className = 'kp-modal-overlay';
            const renderSourcesList = () => {
                const container = overlay.querySelector('.kp-source-list');
                if (!container) return;
                container.innerHTML = '';
                this.data.sources.forEach((src, index) => {
                    const item = document.createElement('div');
                    item.className = `kp-source-item ${src.enabled ? '' : 'disabled'}`;
                    item.innerHTML = `
                        <div class="kp-source-left">
                            <input type="checkbox" class="kp-checkbox" ${src.enabled ? 'checked' : ''}>
                            <span class="kp-source-name" style="color: ${src.color}">${src.name}</span>
                        </div>
                        <div class="kp-source-actions">
                            <button class="kp-action-btn move-up" title="Вверх">⬆️</button>
                            <button class="kp-action-btn move-down" title="Вниз">⬇️</button>
                            ${!DEFAULT_SOURCES.find(d => d.id === src.id) ? '<button class="kp-action-btn delete" title="Удалить">✖</button>' : ''}
                        </div>
                    `;
                    item.querySelector('.kp-checkbox').onchange = (e) => { src.enabled = e.target.checked; item.className = `kp-source-item ${src.enabled ? '' : 'disabled'}`; };
                    item.querySelector('.move-up').onclick = () => { if (index > 0) { [this.data.sources[index], this.data.sources[index - 1]] = [this.data.sources[index - 1], this.data.sources[index]]; renderSourcesList(); } };
                    item.querySelector('.move-down').onclick = () => { if (index < this.data.sources.length - 1) { [this.data.sources[index], this.data.sources[index + 1]] = [this.data.sources[index + 1], this.data.sources[index]]; renderSourcesList(); } };
                    const delBtn = item.querySelector('.delete');
                    if (delBtn) delBtn.onclick = () => { if (confirm(`Удалить кнопку "${src.name}"?`)) { this.data.sources.splice(index, 1); renderSourcesList(); } };
                    container.appendChild(item);
                });
            };
            overlay.innerHTML = `
                <div class="kp-modal">
                    <h3>Настройка кнопок <button class="kp-btn-close">Закрыть</button></h3>
                    <div style="display:flex; align-items:center; gap:10px; padding-bottom:10px;">
                        <input type="checkbox" id="st-newtab" class="kp-checkbox" ${this.data.forceNewTab ? 'checked' : ''}>
                        <label for="st-newtab" style="cursor:pointer">Всегда открывать в новой вкладке</label>
                    </div>
                    <div class="kp-source-list"></div>
                    <div class="kp-add-form">
                        <span style="font-weight:bold; font-size:13px;">Добавить свою кнопку:</span>
                        <div class="kp-form-row">
                            <input type="text" id="new-name" class="kp-input" placeholder="Название">
                            <input type="color" id="new-color" class="kp-input-color" value="#ffffff">
                        </div>
                        <input type="text" id="new-url" class="kp-input" placeholder="Ссылка: https://site.com/watch/{id}">
                        <button class="kp-btn-primary" id="btn-add-source" style="margin-top:5px; font-size:12px; padding:5px;">Добавить</button>
                    </div>
                    <div class="kp-modal-footer"><button class="kp-btn-primary kp-save-btn" style="width:100%">Сохранить настройки</button></div>
                </div>
            `;
            setTimeout(renderSourcesList, 0);
            overlay.onclick = (e) => { if(e.target === overlay) overlay.classList.remove('active'); };
            overlay.querySelector('.kp-btn-close').onclick = () => overlay.classList.remove('active');
            overlay.querySelector('.kp-save-btn').onclick = () => {
                this.data.forceNewTab = overlay.querySelector('#st-newtab').checked;
                this.saveData();
                this.render(true);
                overlay.classList.remove('active');
            };
            overlay.querySelector('#btn-add-source').onclick = () => {
                const name = overlay.querySelector('#new-name').value.trim();
                const url = overlay.querySelector('#new-url').value.trim();
                const color = overlay.querySelector('#new-color').value;
                if (!name || !url) return alert('Заполните поля!');
                this.data.sources.push({ id: 'custom_' + Date.now(), name: name, color: color, template: url, enabled: true });
                overlay.querySelector('#new-name').value = ''; overlay.querySelector('#new-url').value = '';
                renderSourcesList();
            };
            document.body.appendChild(overlay);
        }

        // --- ПОИСК ОРИЕНТИРА НА ОСНОВЕ HTML ПОЛЬЗОВАТЕЛЯ ---
        findAnchor() {
            // 1. Самый точный поиск по классу из HTML пользователя
            const buttonBar = document.querySelector('.style_buttonBar__Su2eL') ||
                              document.querySelector('.styles_root__giEsN'); // Внутренний контейнер

            if (buttonBar) return buttonBar;

            // 2. Поиск кнопки "Еще" (она уникальна для этого меню)
            const moreButton = document.querySelector('button.styles_more__URDt5');
            if (moreButton) {
                // Поднимаемся до общего контейнера
                const parentRow = moreButton.closest('.styles_root__giEsN') ||
                                  moreButton.closest('.style_buttonBar__Su2eL');
                if (parentRow) return parentRow;
            }

            // 3. Фоллбэк на десктопные контейнеры
            const desktopContainer = document.querySelector('[class*="styles_buttonsContainer__"]');
            if (desktopContainer) return desktopContainer;

            return null;
        }

        render(force = false) {
            const kpId = Utils.getId();
            const title = Utils.getTitle();
            if (!kpId) return;

            const globalExisting = document.querySelector('.kp-mirror-wrapper');
            if (globalExisting && !force) return;
            if (globalExisting && force) globalExisting.remove();

            const anchor = this.findAnchor();
            if (!anchor) return; // Если не нашли, куда вставлять, не рисуем

            const wrapper = document.createElement('div');
            wrapper.className = 'kp-mirror-wrapper';

            let hotkeyCounter = 1;
            this.data.sources.forEach(src => {
                if (!src.enabled) return;
                const btn = document.createElement('button');
                btn.className = 'kp-mirror-btn';
                btn.textContent = src.name;
                btn.style.background = src.color;
                if (window.innerWidth > 1024 && hotkeyCounter <= 9) {
                    const hint = document.createElement('span');
                    hint.className = 'kp-hotkey-hint';
                    hint.textContent = `Alt+${hotkeyCounter}`;
                    btn.appendChild(hint);
                    hotkeyCounter++;
                }
                btn.onclick = (e) => { e.preventDefault(); this.openSource(src, kpId, title); };
                wrapper.appendChild(btn);
            });

            const tgBtn = document.createElement('a');
            tgBtn.className = 'kp-mirror-btn kp-telegram-btn';
            tgBtn.href = CONFIG.telegramUrl;
            tgBtn.target = '_blank';
            tgBtn.innerHTML = `<svg class="kp-telegram-icon" viewBox="0 0 24 24"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.562 8.161c-.18 1.897-.962 6.502-1.361 8.627-.168.9-.5 1.201-.82 1.23-.697.064-1.226-.461-1.901-.903-1.056-.692-1.653-1.123-2.678-1.799-1.185-.781-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.139-5.062 3.345-.479.329-.913.489-1.302.481-.428-.008-1.252-.241-1.865-.44-.751-.244-1.349-.374-1.297-.789.027-.216.324-.437.893-.663 3.498-1.524 5.831-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.477-1.635.099-.002.321.023.465.141.119.098.152.228.166.33.016.113.02.325.016.365z"/></svg>Скрипты`;
            wrapper.appendChild(tgBtn);

            const settingsBtn = document.createElement('button');
            settingsBtn.className = 'kp-mirror-btn kp-settings-btn';
            settingsBtn.innerHTML = `<svg class="kp-settings-icon" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>`;
            settingsBtn.onclick = (e) => {
                e.preventDefault();
                this.createSettingsModal();
                setTimeout(() => document.getElementById('kp-settings-modal').classList.add('active'), 10);
            };
            wrapper.appendChild(settingsBtn);

            // ВСТАВКА: Если у нас ButtonBar, вставляем ПОСЛЕ него
            anchor.after(wrapper);
        }

        debouncedRender() {
            if (this.renderTimeout) clearTimeout(this.renderTimeout);
            this.renderTimeout = setTimeout(() => {
                this.cleanGarbage();
                this.render();
            }, 300);
        }

        cleanGarbage() {
            const selectors = ['.kinopoisk-watch-online-button', 'a[href*="/plus/"]', '.styles_subscriptionOffer__eau7V'];
            selectors.forEach(sel => document.querySelectorAll(sel).forEach(el => { el.style.display = 'none'; }));
            document.querySelectorAll('button').forEach(btn => {
                if (btn.textContent && (btn.textContent.includes('Попробовать Плюс') || btn.textContent.includes('Билеты'))) btn.style.display = 'none';
            });
        }

        startObserver() {
            const observer = new MutationObserver((mutations) => {
                const isOwnMutation = Array.from(mutations).every(m => m.target.closest && m.target.closest('.kp-mirror-wrapper'));
                if (isOwnMutation) return;
                let shouldRender = false;
                for (let m of mutations) if (m.addedNodes.length) shouldRender = true;
                if (shouldRender) this.debouncedRender();
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
    }

    if (window.location.hostname.includes('tapeop.dev')) {
        window.addEventListener('load', () => setTimeout(KinopoiskEnhancer.initTapeOpReceiver, 500));
    } else {
        const app = new KinopoiskEnhancer();
        const start = () => { app.debouncedRender(); app.startObserver(); };
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
        else start();
    }

})();