All-in-one: Orange Button on KP/IMDb + Search on Player
// ==UserScript==
// @name Tape Operator 2.0
// @namespace tape-operator
// @author Kirlovon & Max Letov
// @description All-in-one: Orange Button on KP/IMDb + Search on Player
// @version 3.3.4
// @icon https://github.com/Kirlovon/Tape-Operator/raw/main/assets/favicon.png
// @run-at document-idle
// @grant GM.info
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.openInTab
// @grant GM.deleteValue
// @grant GM.xmlHttpRequest
// @match *://www.kinopoisk.ru/*
// @match *://hd.kinopoisk.ru/*
// @match *://*.imdb.com/title/*
// @match *://www.themoviedb.org/movie/*
// @match *://www.themoviedb.org/tv/*
// @match *://letterboxd.com/film/*
// @match *://tapeop.dev/*
// @connect api.themoviedb.org
// @connect lingering-salad-a373.l3towm.workers.dev
// ==/UserScript==
(function () {
'use strict';
// --- КОНФИГУРАЦИЯ ---
const VERSION = "3.3.4";
const PLAYER_URL = 'https://tapeop.dev/';
const BUTTON_ID = 'tape-operator-button';
// ВАЖНО: Твои данные
const TMDB_API_KEY = '5fc153497d26350515c189f71fb16ec0';
const TMDB_PROXY_BASE = 'https://lingering-salad-a373.l3towm.workers.dev/3';
const PROXY_ROOT = 'https://lingering-salad-a373.l3towm.workers.dev';
// URL Matchers
const KINOPOISK_MATCHER = /kinopoisk\.ru\/(film|series)\/.*/;
const IMDB_MATCHER = /imdb\.com\/title\/tt\.*/;
const TMDB_MATCHER = /themoviedb\.org\/(movie|tv)\/\.*/;
const LETTERBOXD_MATCHER = /letterboxd\.com\/film\/\.*/;
const MATCHERS = [KINOPOISK_MATCHER, IMDB_MATCHER, TMDB_MATCHER, LETTERBOXD_MATCHER];
let previousUrl = '/';
// =================================================================================================
// ГЛАВНЫЙ ЗАПУСК
// =================================================================================================
if (location.href.includes('tapeop.dev')) {
initPlayer();
} else {
initButtonLogic();
}
// =================================================================================================
// ЧАСТЬ 1: ЛОГИКА КНОПКИ
// =================================================================================================
function initButtonLogic() {
const observer = new MutationObserver(() => updateButton());
observer.observe(document, { subtree: true, childList: true });
setInterval(() => checkButtonPositions(), 500);
updateButton();
}
function updateButton() {
const url = getCurrentURL();
if (url !== previousUrl) { document.getElementById(BUTTON_ID)?.remove(); }
if (!MATCHERS.some((m) => url.match(m))) return removeButton();
if (document.getElementById(BUTTON_ID)) { previousUrl = url; return; }
if (!extractTitle()) return removeButton();
previousUrl = url;
createAndAttachButton();
}
function checkButtonPositions() {
const url = getCurrentURL();
const btn = document.getElementById(BUTTON_ID);
if (!btn) return;
if (url.includes('imdb.com')) fixImdbPosition(btn);
else if (url.includes('hd.kinopoisk.ru')) fixHdKinopoiskPosition(btn);
}
function fixImdbPosition(btn) {
const targetBtn = document.querySelector('button.ipc-split-button__btn.ipc-split-button__btn--button-radius');
if (targetBtn) {
const wrapper = targetBtn.parentElement;
if (wrapper && btn.nextElementSibling !== wrapper) {
wrapper.parentElement.insertBefore(btn, wrapper);
btn.className = 'tape-op-base tape-op-yellow';
btn.classList.remove('tape-op-fixed');
btn.style.width = '100%'; btn.style.marginBottom = '12px'; btn.style.marginRight = '0';
}
}
}
function fixHdKinopoiskPosition(btn) {
const trailerBtn = document.querySelector('button[class*="styles_button_trailer"]');
if (trailerBtn) {
const parent = trailerBtn.parentElement;
if (parent && btn.nextElementSibling !== trailerBtn) {
parent.insertBefore(btn, trailerBtn);
btn.className = 'tape-op-base tape-op-orange';
btn.classList.remove('tape-op-fixed');
btn.style.width = 'auto'; btn.style.marginBottom = '0'; btn.style.marginLeft = '0'; btn.style.marginRight = '12px';
}
}
}
function createAndAttachButton() {
if (document.getElementById(BUTTON_ID)) return;
if (!document.getElementById('tape-op-btn-styles')) {
const style = document.createElement('style');
style.id = 'tape-op-btn-styles';
style.textContent = `
.tape-op-base { display: flex; align-items: center; justify-content: center; box-sizing: border-box; height: 52px; border-radius: 26px; font-family: 'Graphik LC', Helvetica, Arial, sans-serif; font-weight: 700; font-size: 15px; line-height: 20px; padding: 0 24px; cursor: pointer; border: none; text-decoration: none !important; transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; white-space: nowrap; z-index: 9999; margin-bottom: 10px; }
.tape-op-base:hover { transform: scale(1.02); }
.tape-op-base svg { margin-right: 8px; width: 24px; height: 24px; }
.tape-op-orange { background: linear-gradient(90deg, #ff5b35 0%, #ff9e22 100%); color: #fff !important; box-shadow: 0 4px 12px rgba(255, 91, 53, 0.25); margin-right: 12px; width: auto; display: inline-flex; }
.tape-op-orange:hover { background: linear-gradient(90deg, #ff6b4a 0%, #ffaa3d 100%); box-shadow: 0 6px 16px rgba(255, 91, 53, 0.4); }
.tape-op-orange svg { fill: #fff; }
.tape-op-yellow { background: #F5C518; color: #000 !important; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); }
.tape-op-yellow:hover { background: #E2B616; }
.tape-op-yellow svg { fill: #000; }
.tape-op-fixed { position: fixed; bottom: 20px; right: 20px; width: auto !important; z-index: 2147483647; }
`;
document.head.appendChild(style);
}
const url = getCurrentURL();
const isImdb = url.includes('imdb.com');
const btn = document.createElement('div');
btn.id = BUTTON_ID;
btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg><span>Смотреть онлайн</span>`;
btn.addEventListener('click', () => openPlayerFromSite());
btn.addEventListener('mousedown', (e) => e.button === 1 && openPlayerFromSite(true));
btn.className = `tape-op-base ${isImdb ? 'tape-op-yellow' : 'tape-op-orange'}`;
if (url.includes('kinopoisk.ru')) {
const buttonsContainer = document.querySelector('[class*="styles_buttonsContainer"]');
const foldersButton = document.querySelector('[class^="styles_foldersButton"]');
if (buttonsContainer) buttonsContainer.prepend(btn);
else if (foldersButton?.parentElement) foldersButton.parentElement.prepend(btn);
else { btn.classList.add('tape-op-fixed'); document.body.appendChild(btn); }
} else if (url.includes('letterboxd.com')) {
const actions = document.querySelector('.sidebar');
if (actions) { btn.className = 'tape-op-base tape-op-orange'; actions.prepend(btn); }
else { btn.classList.add('tape-op-fixed'); document.body.appendChild(btn); }
} else {
btn.classList.add('tape-op-fixed'); document.body.appendChild(btn);
}
}
function removeButton() { document.getElementById(BUTTON_ID)?.remove(); }
async function openPlayerFromSite(loadInBackground = false) {
const data = extractMovieData();
if (!data) return;
await GM.setValue('movie-data', data);
GM.openInTab(PLAYER_URL, loadInBackground);
}
// =================================================================================================
// ЧАСТЬ 2: ЛОГИКА ПОИСКА (TAPEOP.DEV)
// =================================================================================================
function initSearchLogic() {
const style = document.createElement('style');
style.textContent = `
#kp-search-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); width: 550px; z-index: 2147483647; font-family: sans-serif; }
#kp-search-input { width: 100%; padding: 14px 44px 14px 24px; border-radius: 28px; border: 1px solid rgba(255,255,255,0.15); background: rgba(18,18,18,0.95); color: #fff; font-size: 16px; outline: none; box-shadow: 0 8px 32px rgba(0,0,0,0.8); backdrop-filter: blur(12px); transition: all 0.2s ease; }
#kp-search-input:focus { background: rgba(25,25,25,1); border-color: #ff6633; box-shadow: 0 8px 40px rgba(255,102,51,0.3); }
#kp-search-results { margin-top: 12px; background: rgba(25,25,25,0.98); border-radius: 16px; overflow: hidden; display: none; box-shadow: 0 10px 50px rgba(0,0,0,0.95); max-height: 500px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.05); }
.kp-result-item { display: flex; align-items: center; padding: 12px 16px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.15s; }
.kp-result-item:last-child { border-bottom: none; }
.kp-result-item:hover { background: rgba(255,255,255,0.15); }
.kp-poster { width: 44px; height: 66px; object-fit: cover; border-radius: 6px; margin-right: 16px; background: #333; flex-shrink: 0; }
.kp-info { display: flex; flex-direction: column; overflow: hidden; flex: 1; }
.kp-title { font-weight: 600; color: #eee; font-size: 15px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kp-meta { color: #aaa; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Убрали фиксированный цвет, оставили только жирность и размер */
.kp-rating { font-weight: 700; margin-left: 10px; font-size: 14px; }
.kp-external-btn {
padding: 6px 10px; margin-left: 12px; border-radius: 8px; background: rgba(255,255,255,0.05);
color: #888; font-size: 11px; font-weight: 600; border: 1px solid rgba(255,255,255,0.1);
transition: all 0.2s; white-space: nowrap; cursor: pointer; text-align: center;
}
.kp-external-btn:hover { background: #ff6633; color: #fff; border-color: #ff6633; }
.kp-status-msg { padding: 15px; text-align: center; color: #888; font-size: 14px; }
.kp-fallback-btn { display: block; width: 100%; padding: 15px; text-align: center; background: #222; color: #ff6633; text-decoration: none; font-weight: 600; cursor: pointer; border-top: 1px solid rgba(255,255,255,0.1); }
.kp-fallback-btn:hover { background: #333; }
#kp-search-results::-webkit-scrollbar { width: 6px; }
#kp-search-results::-webkit-scrollbar-track { background: transparent; }
#kp-search-results::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
`;
document.head.appendChild(style);
const container = document.createElement('div'); container.id = 'kp-search-container';
const input = document.createElement('input'); input.id = 'kp-search-input'; input.placeholder = 'Поиск фильмов и сериалов...'; input.autocomplete = 'off';
const resultsList = document.createElement('div'); resultsList.id = 'kp-search-results';
container.appendChild(input); container.appendChild(resultsList); document.body.appendChild(container);
let debounceTimer;
input.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
const query = e.target.value.trim();
if (query.length < 2) { resultsList.style.display = 'none'; return; }
resultsList.innerHTML = '<div class="kp-status-msg">Ищу...</div>';
resultsList.style.display = 'block';
debounceTimer = setTimeout(() => searchTMDB(query, resultsList, input), 300);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const query = input.value.trim();
if (query) debounceTimer = setTimeout(() => searchTMDB(query, resultsList, input), 100);
}
});
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) resultsList.style.display = 'none';
});
}
function searchTMDB(query, resultsList, input) {
const url = `${TMDB_PROXY_BASE}/search/multi?api_key=${TMDB_API_KEY}&language=ru-RU&query=${encodeURIComponent(query)}&page=1&include_adult=true`;
GM.xmlHttpRequest({
method: "GET", url: url,
onload: (res) => {
try { renderResults(JSON.parse(res.responseText).results, query, resultsList, input); }
catch (e) {
console.error(e);
resultsList.innerHTML = '<div class="kp-status-msg">Ошибка API (Проверь ключ или прокси)</div>';
}
},
onerror: (err) => {
console.error(err);
resultsList.innerHTML = '<div class="kp-status-msg">Ошибка сети (Прокси недоступен)</div>';
}
});
}
function renderResults(movies, query, resultsList, input) {
resultsList.innerHTML = '';
const filtered = (movies || []).filter(m => m.media_type === 'movie' || m.media_type === 'tv');
if (!filtered.length) {
resultsList.innerHTML = '<div class="kp-status-msg">В базе не найдено</div>';
addFallbackButton(query, resultsList);
return;
}
let count = 0;
filtered.forEach(movie => {
if (count >= 6) return; count++;
const item = document.createElement('div'); item.className = 'kp-result-item';
const title = movie.title || movie.name || '???';
const year = (movie.release_date || movie.first_air_date || '').split('-')[0];
const poster = movie.poster_path
? `${PROXY_ROOT}/t/p/w92${movie.poster_path}`
: 'https://via.placeholder.com/44x66/333/888?text=?';
const ratingVal = movie.vote_average || 0;
const ratingText = ratingVal ? ratingVal.toFixed(1) : '';
// --- ДИНАМИЧЕСКИЙ ЦВЕТ ---
// 0 (красный) -> 60 (желтый) -> 120 (зеленый)
// Умножаем рейтинг на 12. Например: 5.0 * 12 = 60 (желтый), 8.0 * 12 = 96 (салатовый), 10 * 12 = 120 (зеленый)
const hue = Math.max(0, Math.min(120, ratingVal * 12));
const ratingColor = `hsl(${hue}, 85%, 50%)`;
item.innerHTML = `
<img src="${poster}" class="kp-poster">
<div class="kp-info"><div class="kp-title">${title}</div><div class="kp-meta">${year} • ${movie.media_type === 'tv' ? 'Сериал' : 'Фильм'}</div></div>
${ratingText ? `<div class="kp-rating" style="color: ${ratingColor}">${ratingText}</div>` : ''}
`;
item.addEventListener('click', async () => {
await GM.setValue('movie-data', { tmdb: movie.id, title: title });
window.location.href = PLAYER_URL;
});
const kpBtn = document.createElement('div');
kpBtn.className = 'kp-external-btn';
kpBtn.innerHTML = 'Кинопоиск ↗';
kpBtn.title = 'Открыть страницу на Кинопоиске';
kpBtn.addEventListener('click', (e) => {
e.stopPropagation();
const searchTitle = title;
const searchYear = year ? ` ${year}` : '';
window.open(`https://www.kinopoisk.ru/index.php?kp_query=${encodeURIComponent(searchTitle + searchYear)}`, '_blank');
});
item.appendChild(kpBtn);
resultsList.appendChild(item);
});
addFallbackButton(query, resultsList);
}
function addFallbackButton(query, list) {
const btn = document.createElement('div'); btn.className = 'kp-fallback-btn';
btn.innerHTML = `🔍 Искать "${query}" на Кинопоиске →`;
btn.onclick = () => window.open(`https://www.kinopoisk.ru/index.php?kp_query=${encodeURIComponent(query)}`, '_blank');
list.appendChild(btn);
}
// =================================================================================================
// ЧАСТЬ 3: ИНИЦИАЛИЗАЦИЯ ПЛЕЕРА
// =================================================================================================
async function initPlayer() {
const data = await GM.getValue('movie-data', {});
await GM.deleteValue('movie-data');
if (!data || Object.keys(data).length === 0) {
initSearchLogic();
return;
}
const dataSerialized = JSON.stringify(JSON.stringify(data));
const versionSerialized = JSON.stringify(VERSION);
const scriptElement = document.createElement('script');
scriptElement.innerHTML = `globalThis.init(JSON.parse(${dataSerialized}), ${versionSerialized});`;
document.body.appendChild(scriptElement);
initSearchLogic();
}
// =================================================================================================
// HELPER FUNCTIONS
// =================================================================================================
function getCurrentURL() { return location.origin + location.pathname; }
function extractTitle() {
try {
const titleElement = document.querySelector('meta[property="og:title"]') || document.querySelector('meta[name="twitter:title"]');
if (!titleElement) return null;
let title = titleElement.content.trim();
if (title.startsWith('Кинопоиск.')) return null;
if (title.includes('— смотреть онлайн')) title = title.replace('— смотреть онлайн в хорошем качестве — Кинопоиск', '').trim();
if (title.includes('⭐')) title = title.split('⭐')[0].trim();
if (title.endsWith('- IMDb') && title.includes(')')) title = title.slice(0, title.lastIndexOf(')') + 1).trim();
return title;
} catch (e) { return null; }
}
function extractMovieData() {
const url = getCurrentURL();
const title = extractTitle();
if (!title) return null;
if (url.match(KINOPOISK_MATCHER)) {
if (url.includes('hd.kinopoisk.ru')) {
try {
const json = JSON.parse(document.getElementById('__NEXT_DATA__').innerText);
const apollo = Object.values(json?.props?.pageProps?.apolloState?.data || {});
const id = apollo.find(i => i?.__typename === 'TvSeries' || i?.__typename === 'Film')?.id;
return id ? { kinopoisk: id, title } : null;
} catch(e) { return null; }
}
return { kinopoisk: url.split('/')[4], title };
}
if (url.match(IMDB_MATCHER)) {
const id = url.split('/')[4];
return { imdb: id, title };
}
if (url.match(TMDB_MATCHER)) {
return { tmdb: url.split('/')[4].split('-')[0], title };
}
if (url.match(LETTERBOXD_MATCHER)) {
const imdb = document.querySelector('a[href*="imdb.com"]')?.href?.split('/')[4];
if (imdb) return { imdb, title };
const tmdb = document.querySelector('a[href*="themoviedb.org"]')?.href?.split('/')[4]?.split('-')[0];
if (tmdb) return { tmdbId: tmdb, title };
}
return null;
}
})();