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

Greasy fork 爱吃馍镜像

Kinopoisk Free Mirror: Customizable v5.1

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

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

公众号二维码

扫码关注【爱吃馍】

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

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

公众号二维码

扫码关注【爱吃馍】

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

// ==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();
    }

})();