您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Embeds Kick.com stream video player into Twitch.tv pages with live status indicators
// ==UserScript== // @name Kick.com to Twitch.tv Embedder // @namespace http://tampermonkey.net/ // @version 0.7.0 // @description Embeds Kick.com stream video player into Twitch.tv pages with live status indicators // @author sushisuish and not AI // @match https://www.twitch.tv/* // @grant GM_xmlhttpRequest // @connect kick.com // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // Config & state const cfg = { channel: 'tapa_tapa_mateo', intervals: {check: 1000, url: 1000, live: 300000}, maxRecent: 5, defaultFavs: [ {name: "Manastore", id: "manastore"}, {name: "Xasmur", id: "xasmur"}, {name: "rolling_typhoon", id: "rolling_typhoon"} ] }; let S = { player: null, orig: null, kick: false, intv: {}, btnAdded: false, recent: [], curKick: "", favs: [], lastUrl: '', isTarget: false, cache: {}, curTwitch: '', clickHandler: null }; // HTML templates & styles const h = { s: { // Status indicators live: `<span class="k-live" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#f00;margin-right:8px;"></span>`, off: `<span class="k-off" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#888;margin-right:8px;"></span>`, err: `<span class="k-err" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#fc0;margin-right:8px;"></span>` }, i: { // Icons k: `<span style="font-size:18px;font-weight:bold;text-align:center;display:inline-block;width:100%;">K</span>`, t: `<span style="display:flex;align-items:center;justify-content:center;width:100%;"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M11.64 5.93h1.43v4.28h-1.43m3.93-4.28H17v4.28h-1.43M7 2L3.43 5.57v12.86h4.28V22l3.58-3.57h2.85L20.57 12V2m-1.43 9.29l-2.85 2.85h-2.86l-2.5 2.5v-2.5H7.71V3.43h11.43Z"/></svg></span>`, dn: `<span class="k-arrow" style="margin-left:5px;width:14px;text-align:center;">▼</span>`, up: `<span class="k-arrow" style="margin-left:5px;width:14px;text-align:center;">▲</span>`, sf: `<svg width="20" height="20" viewBox="0 0 24 24" style="margin-left:8px;cursor:pointer;vertical-align:middle;"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" fill="#FFD700" /></svg>`, se: `<svg width="20" height="20" viewBox="0 0 24 24" style="margin-left:8px;cursor:pointer;vertical-align:middle;"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" fill="none" stroke="#FFD700" stroke-width="1.5" /></svg>`, tr: `<svg width="18" height="18" viewBox="0 0 24 24" style="margin-left:8px;cursor:pointer;vertical-align:middle;"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="#ff6b6b"/></svg>` }, c: { // CSS btn: { base: `position:absolute;top:10px;z-index:9999;color:white;border:none;border-radius:4px;padding:6px 10px;font-weight:bold;cursor:pointer;display:flex;align-items:center;justify-content:center;height:30px;`, main: `background:#00b800;right:25%;min-width:125px;white-space:nowrap;`, min: `background:#00b800;right:calc(25% + 45px);width:40px;transition:width 0.5s ease;`, exp: `background:#00b800;right:calc(25% + 45px);width:auto;transition:width 0.5s ease;`, twitch: `background:#772ce8;right:25%;width:40px;` }, dd: { cont: `position:absolute;background:#18181b;border-radius:4px;box-shadow:0 4px 12px rgba(0,0,0,0.5);padding:8px;z-index:10000;display:none;width:240px;`, item: `padding:8px 12px;cursor:pointer;color:white;border-radius:4px;margin-bottom:4px;font-weight:bold;display:flex;align-items:center;justify-content:space-between;`, sect: `padding:4px 12px;color:#adadb8;font-size:12px;margin-top:6px;margin-bottom:2px;`, inp: `background:#18181b;color:white;border:1px solid #3a3a3d;border-radius:4px;padding:6px 8px;font-size:14px;width:70%;flex-grow:1;`, add: `background:#00b800;color:white;border:none;border-radius:4px;padding:6px 10px;font-weight:bold;cursor:pointer;font-size:12px;`, wrap: `padding:8px 12px;color:#dedee3;border-radius:4px;display:flex;align-items:center;gap:8px;margin-top:4px;` } } }; // Initialize script function init() { loadChannels(); S.lastUrl = location.href; S.intv.url = setInterval(checkUrl, cfg.intervals.url); S.intv.live = setInterval(updateLive, cfg.intervals.live); updateLive(); window.addEventListener('popstate', checkUrl); } // Load & save channels function loadChannels() { try { const r = localStorage.getItem('kickTwitchRecentChannels'); if (r) S.recent = JSON.parse(r); const f = localStorage.getItem('kickTwitchFavoriteChannels'); if (f) { S.favs = JSON.parse(f); } else { S.favs = cfg.defaultFavs; saveChannels(); } } catch (e) { S.recent = []; S.favs = cfg.defaultFavs; } } function saveChannels() { try { localStorage.setItem('kickTwitchRecentChannels', JSON.stringify(S.recent)); localStorage.setItem('kickTwitchFavoriteChannels', JSON.stringify(S.favs)); } catch (e) { console.error('Error saving channels:', e); } } // Check if URL changed and we're on target channel function checkUrl() { const url = location.href; const changed = url !== S.lastUrl; // Get current channel from path const match = location.pathname.match(/^\/([^\/]+)/); const ch = match ? match[1].toLowerCase() : ''; const onTarget = ch === cfg.channel.toLowerCase(); // First time check if (!changed && !S.isTarget && onTarget) { S.curTwitch = ch; S.isTarget = true; if (!S.intv.check) S.intv.check = setInterval(findPlayer, cfg.intervals.check); return; } if (!changed) return; // URL changed S.lastUrl = url; // If on Kick and Twitch channel changed, revert if (S.kick && ch !== S.curTwitch && ch) { if (S.player && S.orig) { S.player.innerHTML = S.orig; S.kick = false; if (ch === cfg.channel.toLowerCase()) addBtn(); } } S.curTwitch = ch; if (onTarget && !S.isTarget) { // Entering target channel S.isTarget = true; if (!S.intv.check) S.intv.check = setInterval(findPlayer, cfg.intervals.check); } else if (!onTarget && S.isTarget) { // Leaving target channel S.isTarget = false; if (S.intv.check) { clearInterval(S.intv.check); S.intv.check = null; } document.querySelectorAll('.k-btn, .k-switch-btn').forEach(b => b.remove()); S.player = null; S.btnAdded = false; } } // Find Twitch player function findPlayer() { if (!S.isTarget) return; const selectors = [ '.video-player__container', '.video-player', '.player-overlay', '.video-ref', '.channel-root .persistent-player', '.channel-info-content .persistent-player', '.persistent-player', '.stream-player', '[data-a-target="video-player"]', '.player', '.video-player__container div[data-a-target="player-overlay"]' ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) { clearInterval(S.intv.check); S.intv.check = null; S.player = el; S.player.style.position = 'relative'; S.player.style.overflow = 'hidden'; if (!S.btnAdded) { addBtn(); S.btnAdded = true; } break; } } } // Check channel live status function updateLive() { // Check favorites & recent channels S.favs.forEach(ch => checkStatus(ch.id)); S.recent.forEach(ch => checkStatus(ch)); // Update button const btn = document.querySelector('.k-btn'); if (btn && btn.dataset.open !== 'true') updateBtnIndicator(btn); } function checkStatus(id) { const now = Date.now(); const cached = S.cache[id]; if (cached && (now - cached.time < 60000)) return; GM_xmlhttpRequest({ method: 'GET', url: `https://kick.com/api/v1/channels/${id}`, headers: {'Accept': 'application/json'}, onload: function(r) { try { const data = JSON.parse(r.responseText); const exists = data && data.id && data.slug && data.user_id; const isLive = exists && data.livestream !== null; S.cache[id] = { live: isLive, exists: exists, time: now }; updateLive(); const dd = document.querySelector('.k-dropdown'); if (dd && dd.style.display !== 'none') updateStatusIcons(); } catch (e) { S.cache[id] = {live: false, exists: false, error: true, time: now}; updateStatusIcons(); } }, onerror: function() { S.cache[id] = {live: false, exists: false, error: true, time: now}; updateStatusIcons(); } }); } // Update button indicator (live dot or arrow) function updateBtnIndicator(btn) { // Check if any channels are live let anyLive = false; // Check favorites then recent channels for (const ch of [...S.favs, ...S.recent.map(id => ({id}))]) { const cached = S.cache[ch.id]; if (cached && cached.live && cached.exists) { anyLive = true; break; } } btn.dataset.anyLive = anyLive ? 'true' : 'false'; const ind = btn.querySelector('.indicator'); if (ind) { ind.innerHTML = anyLive ? '<span style="width:10px;height:10px;border-radius:50%;background:#f00;display:inline-block;"></span>' : '▼'; } } // Update status indicators in dropdown function updateStatusIcons() { document.querySelectorAll('.k-channel-item').forEach(item => { const id = item.dataset.channelId; if (!id) return; let ind = item.querySelector('.k-live, .k-off, .k-err'); if (!ind) { ind = document.createElement('span'); item.prepend(ind); } const cached = S.cache[id]; if (!cached) { ind.outerHTML = h.s.off; } else if (cached.error || !cached.exists) { ind.outerHTML = h.s.err; const newInd = item.querySelector('.k-err'); if (newInd) { newInd.title = cached.exists === false ? "Channel doesn't exist" : "Error checking channel"; } } else if (cached.live) { ind.outerHTML = h.s.live; } else { ind.outerHTML = h.s.off; } }); } // Add the main Kick button function addBtn() { if (!S.player) return; document.querySelectorAll('.k-btn, .k-switch-btn').forEach(b => b.remove()); // Check for any live channels let anyLive = false; for (const ch of [...S.favs, ...S.recent.map(id => ({id}))]) { const cached = S.cache[ch.id]; if (cached && cached.live && cached.exists) { anyLive = true; break; } } const btn = document.createElement('button'); btn.classList.add('k-btn'); btn.dataset.open = 'false'; btn.dataset.anyLive = anyLive ? 'true' : 'false'; btn.setAttribute('style', h.c.btn.base + h.c.btn.main); btn.innerHTML = ` <div style="display:flex;align-items:center;justify-content:space-between;width:100%;"> <span>Switch to</span> <span style="font-size:18px;font-weight:bold;padding:0 5px;">K</span> <span class="indicator" style="width:14px;display:inline-flex;align-items:center;justify-content:center;"> ${anyLive ? '<span style="width:10px;height:10px;border-radius:50%;background:#f00;display:inline-block;"></span>' : '▼'} </span> </div> `; btn.addEventListener('click', e => { e.stopPropagation(); const isOpen = btn.dataset.open === 'true'; document.querySelectorAll('.k-dropdown').forEach(d => d.remove()); if (S.clickHandler) { document.removeEventListener('click', S.clickHandler); S.clickHandler = null; } const ind = btn.querySelector('.indicator'); if (isOpen) { btn.dataset.open = 'false'; updateBtnIndicator(btn); return; } btn.dataset.open = 'true'; ind.innerHTML = '▲'; createDropdown(btn.getBoundingClientRect(), btn); }); document.body.appendChild(btn); } // Add switch channel buttons (when on Kick) function addSwitchBtns() { if (!S.player) return; document.querySelectorAll('.k-btn, .k-switch-btn').forEach(b => b.remove()); // Back to Twitch button const backBtn = document.createElement('button'); backBtn.innerHTML = h.i.t; backBtn.setAttribute('style', h.c.btn.base + h.c.btn.twitch); backBtn.classList.add('k-btn'); backBtn.addEventListener('click', () => { S.player.innerHTML = S.orig; S.kick = false; addBtn(); }); // Switch Kick channel button const switchBtn = document.createElement('button'); switchBtn.innerHTML = h.i.k; switchBtn.setAttribute('style', h.c.btn.base + h.c.btn.min); switchBtn.classList.add('k-switch-btn'); switchBtn.dataset.open = 'false'; // Hover effects switchBtn.addEventListener('mouseenter', function() { this.innerHTML = ''; const text = document.createElement('span'); text.textContent = 'Switch Channel '; this.appendChild(text); const kLogo = document.createElement('span'); kLogo.innerHTML = `<span style="font-size:18px;font-weight:bold;margin-left:4px;">K</span>`; this.appendChild(kLogo); const arrow = document.createElement('span'); arrow.innerHTML = this.dataset.open === 'true' ? h.i.up : h.i.dn; arrow.classList.add('k-arrow'); this.appendChild(arrow); this.setAttribute('style', h.c.btn.base + h.c.btn.exp); }); switchBtn.addEventListener('mouseleave', function() { if (this.dataset.open !== 'true') { this.innerHTML = h.i.k; this.setAttribute('style', h.c.btn.base + h.c.btn.min); } }); // Dropdown toggle switchBtn.addEventListener('click', e => { e.stopPropagation(); const isOpen = switchBtn.dataset.open === 'true'; document.querySelectorAll('.k-dropdown').forEach(d => d.remove()); if (S.clickHandler) { document.removeEventListener('click', S.clickHandler); S.clickHandler = null; } if (isOpen) { switchBtn.dataset.open = 'false'; if (!switchBtn.matches(':hover')) { switchBtn.innerHTML = h.i.k; switchBtn.setAttribute('style', h.c.btn.base + h.c.btn.min); } else { const arrow = switchBtn.querySelector('.k-arrow'); if (arrow) arrow.innerHTML = h.i.dn; } return; } switchBtn.dataset.open = 'true'; if (switchBtn.innerHTML === h.i.k) { switchBtn.innerHTML = ''; const text = document.createElement('span'); text.textContent = 'Switch Channel '; switchBtn.appendChild(text); const kLogo = document.createElement('span'); kLogo.innerHTML = `<span style="font-size:18px;font-weight:bold;margin-left:4px;">K</span>`; switchBtn.appendChild(kLogo); const arrow = document.createElement('span'); arrow.innerHTML = h.i.up; arrow.classList.add('k-arrow'); switchBtn.appendChild(arrow); } else { const arrow = switchBtn.querySelector('.k-arrow'); if (arrow) arrow.innerHTML = h.i.up; } switchBtn.setAttribute('style', h.c.btn.base + h.c.btn.exp); createDropdown(switchBtn.getBoundingClientRect(), switchBtn); }); document.body.appendChild(backBtn); document.body.appendChild(switchBtn); } // Create dropdown with channel list function createDropdown(rect, parentBtn) { const dd = document.createElement('div'); dd.setAttribute('style', h.c.dd.cont); dd.classList.add('k-dropdown'); dd.style.top = (rect.bottom + window.scrollY) + 'px'; dd.style.left = (rect.left + window.scrollX) + 'px'; dd.style.display = 'block'; // Add favorites section if (S.favs.length > 0) { const header = document.createElement('div'); header.textContent = 'FAVORITE CHANNELS'; header.setAttribute('style', h.c.dd.sect); dd.appendChild(header); // Add favorite channels S.favs.forEach(ch => { dd.appendChild(createChannelItem(ch.id, ch.name, true, rect, parentBtn, dd)); }); } // Add recent channels section if (S.recent.length > 0) { const header = document.createElement('div'); header.textContent = 'RECENT CHANNELS'; header.setAttribute('style', h.c.dd.sect); dd.appendChild(header); S.recent.forEach(ch => { dd.appendChild(createChannelItem(ch, ch, false, rect, parentBtn, dd)); }); } // Add separator const sep = document.createElement('div'); sep.setAttribute('style', 'border-top:1px solid #3a3a3d;margin:8px 0;'); dd.appendChild(sep); // Add custom channel input const container = document.createElement('div'); container.setAttribute('style', h.c.dd.wrap); const input = document.createElement('input'); input.setAttribute('type', 'text'); input.setAttribute('placeholder', 'Custom channel...'); input.setAttribute('style', h.c.dd.inp); const addBtn = document.createElement('button'); addBtn.textContent = 'Add'; addBtn.setAttribute('style', h.c.dd.add); input.addEventListener('click', e => e.stopPropagation()); input.addEventListener('keypress', e => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); const channel = input.value.trim(); if (channel) validateChannel(channel, dd); } }); addBtn.addEventListener('click', e => { e.stopPropagation(); const channel = input.value.trim(); if (channel) validateChannel(channel, dd); }); container.appendChild(input); container.appendChild(addBtn); dd.appendChild(container); // Close dropdown on outside click S.clickHandler = e => { if (!dd.contains(e.target) && !e.target.classList.contains('k-btn') && !e.target.classList.contains('k-switch-btn')) { document.querySelectorAll('.k-dropdown').forEach(d => d.style.display = 'none'); // Update buttons document.querySelectorAll('.k-btn').forEach(btn => { if (btn.dataset.open === 'true') { btn.dataset.open = 'false'; updateBtnIndicator(btn); } }); document.querySelectorAll('.k-switch-btn').forEach(btn => { if (btn.dataset.open === 'true') { btn.dataset.open = 'false'; if (!btn.matches(':hover')) { btn.innerHTML = h.i.k; btn.setAttribute('style', h.c.btn.base + h.c.btn.min); } else { const arrow = btn.querySelector('.k-arrow'); if (arrow) arrow.innerHTML = h.i.dn; } } }); document.removeEventListener('click', S.clickHandler); S.clickHandler = null; } }; document.addEventListener('click', S.clickHandler); document.body.appendChild(dd); updateStatusIcons(); return dd; } // Create a channel item for the dropdown function createChannelItem(id, name, isFav, rect, parentBtn, dd) { const item = document.createElement('div'); item.setAttribute('style', h.c.dd.item + `background:${isFav ? '#00b800' : '#006600'};`); item.classList.add('k-channel-item'); item.dataset.channelId = id; // Add status indicator const cached = S.cache[id]; let status = h.s.off; if (cached) { if (cached.error || !cached.exists) { status = h.s.err; } else if (cached.live) { status = h.s.live; } } item.innerHTML = status; // Channel name const nameSpan = document.createElement('span'); nameSpan.textContent = name; nameSpan.style.flexGrow = '1'; nameSpan.style.cursor = 'pointer'; // Trash icon const trashIcon = document.createElement('span'); trashIcon.innerHTML = h.i.tr; trashIcon.title = 'Remove from list'; trashIcon.style.display = 'flex'; trashIcon.style.alignItems = 'center'; // Star icon const starIcon = document.createElement('span'); starIcon.innerHTML = isFav ? h.i.sf : h.i.se; starIcon.title = isFav ? 'Remove from favorites' : 'Add to favorites'; starIcon.style.display = 'flex'; starIcon.style.alignItems = 'center'; // Add elements item.appendChild(nameSpan); item.appendChild(trashIcon); item.appendChild(starIcon); // Channel name click nameSpan.addEventListener('click', e => { e.stopPropagation(); dd.style.display = 'none'; embedKick(id); }); // Trash icon click trashIcon.addEventListener('click', e => { e.stopPropagation(); if (isFav) { if (confirm(`Remove "${name}" from your channels?`)) { S.favs = S.favs.filter(ch => ch.id.toLowerCase() !== id.toLowerCase()); saveChannels(); item.style.display = 'none'; setTimeout(() => { dd.style.display = 'none'; createDropdown(rect, parentBtn); }, 10); } } else { // Remove from recent without confirmation S.recent = S.recent.filter(ch => ch.toLowerCase() !== id.toLowerCase()); saveChannels(); item.style.display = 'none'; if (S.recent.length === 0) { setTimeout(() => { dd.style.display = 'none'; createDropdown(rect, parentBtn); }, 10); } } }); // Star icon click starIcon.addEventListener('click', e => { e.stopPropagation(); if (isFav) { S.favs = S.favs.filter(ch => ch.id.toLowerCase() !== id.toLowerCase()); saveChannels(); item.style.display = 'none'; } else { if (!S.favs.some(ch => ch.id.toLowerCase() === id.toLowerCase())) { S.favs.push({id, name}); S.recent = S.recent.filter(ch => ch.toLowerCase() !== id.toLowerCase()); saveChannels(); item.style.display = 'none'; } } setTimeout(() => { dd.style.display = 'none'; createDropdown(rect, parentBtn); }, 10); }); return item; } // Validate a channel before adding function validateChannel(channelName, dd) { const cached = S.cache[channelName]; const now = Date.now(); if (cached && now - cached.time < 60000) { if (cached.exists === false) { alert(`Channel "${channelName}" doesn't exist on Kick.com.`); return; } else { dd.style.display = 'none'; embedKick(channelName); } } else { // Show loading message const loading = document.createElement('div'); loading.textContent = `Validating channel "${channelName}"...`; loading.style.position = 'fixed'; loading.style.transform = 'translate(-50%, -50%)'; loading.style.backgroundColor = '#18181b'; loading.style.color = 'white'; loading.style.padding = '10px 20px'; loading.style.borderRadius = '4px'; loading.style.zIndex = '10001'; document.body.appendChild(loading); GM_xmlhttpRequest({ method: 'GET', url: `https://kick.com/api/v1/channels/${channelName}`, headers: {'Accept': 'application/json'}, onload: function(r) { document.body.removeChild(loading); try { const data = JSON.parse(r.responseText); const exists = data && data.id && data.slug && data.user_id; S.cache[channelName] = { live: exists && data.livestream !== null, exists: exists, time: now }; if (exists) { dd.style.display = 'none'; embedKick(channelName); } else { alert(`Channel "${channelName}" doesn't exist on Kick.com.`); } } catch (e) { alert(`Error validating channel "${channelName}". Please try again.`); S.cache[channelName] = { live: false, exists: false, error: true, time: now }; } }, onerror: function() { document.body.removeChild(loading); alert(`Error connecting to Kick.com. Please check your connection.`); S.cache[channelName] = { live: false, exists: false, error: true, time: now }; } }); } } // Embed Kick player function embedKick(channelName) { const cached = S.cache[channelName]; if (cached && cached.exists === false) { alert(`Channel "${channelName}" doesn't exist on Kick.com.`); return; } S.curKick = channelName; // Add to recent channels if not a favorite if (!S.favs.some(ch => ch.id.toLowerCase() === channelName.toLowerCase())) { S.recent = S.recent.filter(ch => ch.toLowerCase() !== channelName.toLowerCase()); S.recent.unshift(channelName); if (S.recent.length > cfg.maxRecent) { S.recent = S.recent.slice(0, cfg.maxRecent); } saveChannels(); checkStatus(channelName); } if (!S.orig) { S.orig = S.player.innerHTML; } const container = document.createElement('div'); container.style.width = '100%'; container.style.height = '100%'; container.style.position = 'relative'; container.style.overflow = 'hidden'; container.innerHTML = ` <iframe id="kick-iframe" height="100%" width="100%" scrolling="no" style="border:none;" src="https://kick.com/${channelName}"> </iframe> `; S.player.innerHTML = ''; S.player.appendChild(container); S.kick = true; addSwitchBtns(); } // Start the script init(); })();