TTK - Twitch To Kick: Multi Stream Auto Switcher

Elegantly switches between Twitch and Kick streams with advanced management features, custom styling, and improved performance

// ==UserScript==
// @name         TTK - Twitch To Kick: Multi Stream Auto Switcher 
// @namespace    http://tampermonkey.net/
// @version      4.1.0
// @description  Elegantly switches between Twitch and Kick streams with advanced management features, custom styling, and improved performance
// @author       Original: TheWhaleCow, Enhanced: Claude
// @match        https://www.twitch.tv/*
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function() {
  'use strict';

  // =================== CONFIGURATION ===================
  const CONFIG = {
    storageKey: 'ttk-streamer-pairs',
    collapsedKey: 'ttk-collapsed',
    settingsVisibleKey: 'ttk-settings-visible',
    currentModeKey: 'ttk-current-mode',
    autoSwitchKey: 'ttk-auto-switch',
    checkInterval: 2000,
    initDelay: 1500,
    theme: {
      primary: '#9146FF',      // Twitch Purple
      secondary: '#00B140',    // Kick Green
      dark: '#18181B',         // Dark background
      light: '#EFEFF1',        // Light text
      border: '#3A3A3D',       // Border color
      success: '#00B140',      // Success color
      error: '#F43B47',        // Error color
      highlight: '#772CE8',    // Highlight color
    }
  };

  // =================== DATA MANAGEMENT ===================
  const Storage = {
    get: (key, defaultValue = null) => {
      try {
        const data = localStorage.getItem(key);
        return data ? JSON.parse(data) : defaultValue;
      } catch (e) {
        console.error('TTK Storage error:', e);
        return defaultValue;
      }
    },

    set: (key, value) => {
      try {
        localStorage.setItem(key, JSON.stringify(value));
        return true;
      } catch (e) {
        console.error('TTK Storage error:', e);
        return false;
      }
    },

    getPairs: () => Storage.get(CONFIG.storageKey, []),
    savePairs: (pairs) => Storage.set(CONFIG.storageKey, pairs),

    isCollapsed: () => Storage.get(CONFIG.collapsedKey, false),
    setCollapsed: (state) => Storage.set(CONFIG.collapsedKey, state),

    isSettingsVisible: () => Storage.get(CONFIG.settingsVisibleKey, false),
    setSettingsVisible: (state) => Storage.set(CONFIG.settingsVisibleKey, state),

    getCurrentMode: () => Storage.get(CONFIG.currentModeKey, 'auto'),
    setCurrentMode: (mode) => Storage.set(CONFIG.currentModeKey, mode),

    isAutoSwitchEnabled: () => Storage.get(CONFIG.autoSwitchKey, true),
    setAutoSwitchEnabled: (state) => Storage.set(CONFIG.autoSwitchKey, state)
  };

  // =================== STREAM MANAGEMENT ===================
  const StreamManager = {
    getCurrentChannel: () => {
      const match = window.location.pathname.match(/^\/([a-zA-Z0-9_]+)$/);
      return match ? match[1].toLowerCase() : null;
    },

    getPairForCurrentChannel: () => {
      const currentChannel = StreamManager.getCurrentChannel();
      if (!currentChannel) return null;

      const pairs = Storage.getPairs();
      return pairs.find(p => p.twitch?.toLowerCase() === currentChannel);
    },

    isTwitchPlayerOffline: () => {
      // Multiple selectors for better reliability
      return !!(
        document.querySelector('[data-test-selector="offline-channel-video"]') ||
        document.querySelector('.channel-status-info [data-a-target="player-overlay-content-gate"]') ||
        Array.from(document.querySelectorAll('.channel-info-content')).some(el =>
          el.textContent.includes('offline') || el.textContent.includes('not available')
        )
      );
    },

    kickStreamURL: (channel) => `https://player.kick.com/${channel}?muted=false`,
    twitchStreamURL: (channel) => `https://player.twitch.tv/?channel=${channel}&parent=twitch.tv&muted=false`,

    replacePlayerWithStream: (url) => {
      const player = document.querySelector('.video-player__container');
      if (!player) return false;

      // Check if we already have our custom player
      const existingOverlay = document.getElementById('ttk-player-overlay');
      if (existingOverlay) {
        const existingIframe = existingOverlay.querySelector('iframe');
        if (existingIframe && existingIframe.src === url) return false;

        existingOverlay.innerHTML = '';
      } else {
        // Create overlay
        const overlay = document.createElement('div');
        overlay.id = 'ttk-player-overlay';
        overlay.style.cssText = `
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          z-index: 9999;
          background: #000;
        `;
        player.style.position = 'relative';
        player.appendChild(overlay);
      }

      // Try to pause/mute all video and audio elements in the original player
      const videoElements = player.querySelectorAll('video');
      videoElements.forEach(video => {
        if (video) {
          try {
            if (!video.paused) video.pause();
            video.muted = true;
            video.volume = 0;
            // Disconnect any event listeners if possible
            if (video.pause) video.pause = () => {};
            if (video.play) video.play = () => {};
          } catch (e) {
            console.error('Error pausing video:', e);
          }
        }
      });

      // Try to pause the player if it has a React instance
      try {
        const twitchPlayer = document.querySelector('[data-a-player-type="site"]');
        if (twitchPlayer && twitchPlayer._reactInstance && typeof twitchPlayer._reactInstance.pausePlayer === 'function') {
          twitchPlayer._reactInstance.pausePlayer();
        }
      } catch (e) {
        console.error('Error accessing Twitch player:', e);
      }

      // Create iframe in the overlay
      const iframe = document.createElement('iframe');
      iframe.src = url;
      iframe.style.cssText = 'height:100%; width:100%; border:none;';
      iframe.setAttribute('allowfullscreen', 'true');
      document.getElementById('ttk-player-overlay').appendChild(iframe);

      UI.showNotification(`Stream changed to ${url.includes('kick') ? 'Kick' : 'Twitch'}`);
      return true;
    },

    switchToTwitch: () => {
      const current = StreamManager.getCurrentChannel();
      if (!current) return false;

      Storage.setCurrentMode('twitch');
      UI.updateModeIndicator('twitch');
      return StreamManager.replacePlayerWithStream(StreamManager.twitchStreamURL(current));
    },

    switchToKick: () => {
      const pair = StreamManager.getPairForCurrentChannel();
      if (!pair || !pair.kick) return false;

      Storage.setCurrentMode('kick');
      UI.updateModeIndicator('kick');
      return StreamManager.replacePlayerWithStream(StreamManager.kickStreamURL(pair.kick));
    },

    autoSwitch: () => {
      if (!Storage.isAutoSwitchEnabled()) return;

      const currentChannel = StreamManager.getCurrentChannel();
      if (!currentChannel) return;

      const pair = StreamManager.getPairForCurrentChannel();
      if (!pair || !pair.kick) return;

      const currentMode = Storage.getCurrentMode();

      // If mode is already set to kick or twitch, respect that choice
      if (currentMode === 'kick') {
        StreamManager.switchToKick();
        return;
      } else if (currentMode === 'twitch') {
        StreamManager.switchToTwitch();
        return;
      }

      // Auto mode - check if Twitch stream is offline
      if (StreamManager.isTwitchPlayerOffline()) {
        StreamManager.switchToKick();
      } else {
        StreamManager.switchToTwitch();
      }
    }
  };

  // =================== USER INTERFACE ===================
  const UI = {
    styles: `
      .ttk-container {
        position: fixed;
        right: 20px;
        top: 80px;
        display: flex;
        flex-direction: column;
        gap: 8px;
        z-index: 9999;
        font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
        transition: transform 0.3s ease;
      }

      .ttk-collapsed {
        transform: translateX(calc(100% + 10px));
      }

      .ttk-button {
        padding: 8px 12px;
        font-size: 13px;
        font-weight: 600;
        border-radius: 6px;
        color: white;
        border: none;
        cursor: pointer;
        user-select: none;
        transition: all 0.2s ease;
        width: 160px;
        display: flex;
        align-items: center;
        gap: 6px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
      }

      .ttk-button:hover {
        transform: translateY(-2px);
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
      }

      .ttk-button:active {
        transform: translateY(0);
      }

      .ttk-twitch-button {
        background-color: ${CONFIG.theme.primary};
      }

      .ttk-twitch-button:hover {
        background-color: #772CE8;
      }

      .ttk-kick-button {
        background-color: ${CONFIG.theme.secondary};
      }

      .ttk-kick-button:hover {
        background-color: #008A2D;
      }

      .ttk-auto-button {
        background-color: #555555;
      }

      .ttk-auto-button:hover {
        background-color: #666666;
      }

      .ttk-icon {
        font-size: 16px;
        display: inline-flex;
      }

      .ttk-indicator {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        margin-left: auto;
        background-color: #888;
      }

      .ttk-indicator.active {
        background-color: #4ade80;
        box-shadow: 0 0 6px #4ade80;
      }

      .ttk-toggle {
        position: fixed;
        top: 120px;
        right: 20px;
        z-index: 10000;
        background: ${CONFIG.theme.dark};
        color: ${CONFIG.theme.light};
        border: 1px solid ${CONFIG.theme.border};
        border-radius: 50%;
        width: 32px;
        height: 32px;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        transition: all 0.2s ease;
        font-size: 14px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
      }

      .ttk-toggle:hover {
        background: #333;
        transform: scale(1.1);
      }

      .ttk-settings-button {
        position: fixed;
        top: 160px;
        right: 20px;
        z-index: 10000;
        background: ${CONFIG.theme.dark};
        color: ${CONFIG.theme.light};
        border: 1px solid ${CONFIG.theme.border};
        border-radius: 50%;
        width: 32px;
        height: 32px;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        transition: all 0.2s ease;
        font-size: 14px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
      }

      .ttk-settings-button:hover {
        background: #333;
        transform: scale(1.1);
      }

      .ttk-settings-panel {
        position: fixed;
        top: 200px;
        right: 20px;
        width: 320px;
        background: ${CONFIG.theme.dark};
        color: ${CONFIG.theme.light};
        border: 1px solid ${CONFIG.theme.border};
        border-radius: 10px;
        padding: 16px;
        z-index: 10000;
        font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        display: none;
        max-height: 60vh;
        overflow-y: auto;
      }

      .ttk-settings-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid ${CONFIG.theme.border};
      }

      .ttk-settings-title {
        font-size: 16px;
        font-weight: 600;
        margin: 0;
      }

      .ttk-close-button {
        background: none;
        border: none;
        color: ${CONFIG.theme.light};
        cursor: pointer;
        font-size: 18px;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
      }

      .ttk-pair {
        background: #252528;
        border-radius: 6px;
        padding: 12px;
        margin-bottom: 12px;
        border: 1px solid ${CONFIG.theme.border};
      }

      .ttk-pair-header {
        display: flex;
        justify-content: space-between;
        margin-bottom: 8px;
      }

      .ttk-pair-number {
        font-weight: 600;
        font-size: 14px;
      }

      .ttk-delete-pair {
        background: none;
        border: none;
        color: #f87171;
        cursor: pointer;
        font-size: 14px;
        padding: 0;
      }

      .ttk-input-group {
        margin-bottom: 8px;
      }

      .ttk-input-label {
        display: block;
        margin-bottom: 4px;
        font-size: 12px;
        color: #a0a0a5;
      }

      .ttk-input {
        width: 100%;
        padding: 8px;
        border-radius: 4px;
        border: 1px solid ${CONFIG.theme.border};
        background: #1a1a1c;
        color: ${CONFIG.theme.light};
        font-size: 13px;
      }

      .ttk-input:focus {
        outline: none;
        border-color: ${CONFIG.theme.highlight};
      }

      .ttk-button-row {
        display: flex;
        gap: 8px;
        margin-top: 16px;
      }

      .ttk-action-button {
        flex: 1;
        padding: 8px 12px;
        border-radius: 4px;
        border: none;
        font-size: 13px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s ease;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 4px;
      }

      .ttk-primary-button {
        background: ${CONFIG.theme.primary};
        color: white;
      }

      .ttk-primary-button:hover {
        background: #772CE8;
      }

      .ttk-secondary-button {
        background: #3A3A3D;
        color: ${CONFIG.theme.light};
      }

      .ttk-secondary-button:hover {
        background: #4A4A4D;
      }

      .ttk-danger-button {
        background: #dc2626;
        color: white;
      }

      .ttk-danger-button:hover {
        background: #b91c1c;
      }

      .ttk-switch-container {
        display: flex;
        align-items: center;
        margin: 16px 0;
        justify-content: space-between;
      }

      .ttk-switch-label {
        font-size: 14px;
      }

      .ttk-switch {
        position: relative;
        display: inline-block;
        width: 46px;
        height: 24px;
      }

      .ttk-switch input {
        opacity: 0;
        width: 0;
        height: 0;
      }

      .ttk-slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #3A3A3D;
        transition: .4s;
        border-radius: 34px;
      }

      .ttk-slider:before {
        position: absolute;
        content: "";
        height: 18px;
        width: 18px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        transition: .4s;
        border-radius: 50%;
      }

      input:checked + .ttk-slider {
        background-color: ${CONFIG.theme.secondary};
      }

      input:focus + .ttk-slider {
        box-shadow: 0 0 1px ${CONFIG.theme.secondary};
      }

      input:checked + .ttk-slider:before {
        transform: translateX(22px);
      }

      .ttk-notification {
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: rgba(0, 0, 0, 0.8);
        color: white;
        padding: 10px 16px;
        border-radius: 6px;
        font-size: 14px;
        z-index: 10001;
        opacity: 0;
        transform: translateY(10px);
        transition: all 0.3s ease;
        font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
      }

      .ttk-notification.show {
        opacity: 1;
        transform: translateY(0);
      }

      #ttk-player-overlay {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 9;
        background: #000;
      }
    `,

    init: () => {
      // Add styles
      if (typeof GM_addStyle !== 'undefined') {
        GM_addStyle(UI.styles);
      } else {
        const style = document.createElement('style');
        style.textContent = UI.styles;
        document.head.appendChild(style);
      }

      UI.createButtons();
      UI.createToggles();
      UI.createSettingsPanel();
    },

    createButtons: () => {
      const container = document.createElement('div');
      container.id = 'ttk-container';
      container.className = 'ttk-container';
      if (Storage.isCollapsed()) container.classList.add('ttk-collapsed');

      // Kick button
      const kickBtn = document.createElement('button');
      kickBtn.className = 'ttk-button ttk-kick-button';
      kickBtn.innerHTML = '<span class="ttk-icon">⚡</span> Switch to Kick <span class="ttk-indicator" id="ttk-kick-indicator"></span>';
      kickBtn.onclick = StreamManager.switchToKick;

      // Twitch button
      const twitchBtn = document.createElement('button');
      twitchBtn.className = 'ttk-button ttk-twitch-button';
      twitchBtn.innerHTML = '<span class="ttk-icon">👾</span> Switch to Twitch <span class="ttk-indicator" id="ttk-twitch-indicator"></span>';
      twitchBtn.onclick = StreamManager.switchToTwitch;

      // Auto button
      const autoBtn = document.createElement('button');
      autoBtn.className = 'ttk-button ttk-auto-button';
      autoBtn.innerHTML = '<span class="ttk-icon">🔄</span> Auto Switch <span class="ttk-indicator" id="ttk-auto-indicator"></span>';
      autoBtn.onclick = () => {
        Storage.setCurrentMode('auto');
        UI.updateModeIndicator('auto');
        StreamManager.autoSwitch();
      };

      container.appendChild(twitchBtn);
      container.appendChild(kickBtn);
      container.appendChild(autoBtn);
      document.body.appendChild(container);

      UI.updateModeIndicator(Storage.getCurrentMode());
    },

    updateModeIndicator: (mode) => {
      const twitchIndicator = document.getElementById('ttk-twitch-indicator');
      const kickIndicator = document.getElementById('ttk-kick-indicator');
      const autoIndicator = document.getElementById('ttk-auto-indicator');

      if (twitchIndicator) twitchIndicator.classList.toggle('active', mode === 'twitch');
      if (kickIndicator) kickIndicator.classList.toggle('active', mode === 'kick');
      if (autoIndicator) autoIndicator.classList.toggle('active', mode === 'auto');
    },

    createToggles: () => {
      // Collapse toggle button
      const toggleBtn = document.createElement('button');
      toggleBtn.className = 'ttk-toggle';
      toggleBtn.id = 'ttk-toggle';
      toggleBtn.innerText = Storage.isCollapsed() ? '❯' : '❮';
      toggleBtn.onclick = () => {
        const container = document.getElementById('ttk-container');
        const isCollapsed = container.classList.contains('ttk-collapsed');
        container.classList.toggle('ttk-collapsed', !isCollapsed);
        toggleBtn.innerText = isCollapsed ? '❮' : '❯';
        Storage.setCollapsed(!isCollapsed);
      };
      document.body.appendChild(toggleBtn);

      // Settings button
      const settingsBtn = document.createElement('button');
      settingsBtn.className = 'ttk-settings-button';
      settingsBtn.innerText = '⚙️';
      settingsBtn.onclick = () => {
        const panel = document.getElementById('ttk-settings-panel');
        const isVisible = panel.style.display === 'block';
        panel.style.display = isVisible ? 'none' : 'block';
        Storage.setSettingsVisible(!isVisible);

        if (!isVisible) {
          UI.refreshPairsList();
        }
      };
      document.body.appendChild(settingsBtn);
    },

    createSettingsPanel: () => {
      const panel = document.createElement('div');
      panel.id = 'ttk-settings-panel';
      panel.className = 'ttk-settings-panel';
      if (Storage.isSettingsVisible()) panel.style.display = 'block';

      // Header
      const headerHTML = `
        <div class="ttk-settings-header">
          <h3 class="ttk-settings-title">TTK Stream Switcher Settings</h3>
          <button class="ttk-close-button" id="ttk-close-settings">×</button>
        </div>
      `;

      // Auto-switch toggle
      const autoSwitchHTML = `
        <div class="ttk-switch-container">
          <span class="ttk-switch-label">Enable auto-switching</span>
          <label class="ttk-switch">
            <input type="checkbox" id="ttk-auto-switch-toggle" ${Storage.isAutoSwitchEnabled() ? 'checked' : ''}>
            <span class="ttk-slider"></span>
          </label>
        </div>
      `;

      // Pairs list container
      const pairsListHTML = `
        <div id="ttk-pairs-container"></div>
      `;

      // Action buttons
      const buttonsHTML = `
        <div class="ttk-button-row">
          <button id="ttk-add-pair" class="ttk-action-button ttk-primary-button">
            <span>+</span> Add Pair
          </button>
          <button id="ttk-save-all" class="ttk-action-button ttk-secondary-button">
            <span>💾</span> Save All
          </button>
        </div>
      `;

      panel.innerHTML = headerHTML + autoSwitchHTML + pairsListHTML + buttonsHTML;
      document.body.appendChild(panel);

      // Event listeners
      document.getElementById('ttk-close-settings').onclick = () => {
        panel.style.display = 'none';
        Storage.setSettingsVisible(false);
      };

      document.getElementById('ttk-auto-switch-toggle').onchange = (e) => {
        Storage.setAutoSwitchEnabled(e.target.checked);
      };

      document.getElementById('ttk-add-pair').onclick = () => {
        const pairs = Storage.getPairs();
        pairs.push({ twitch: '', kick: '' });
        Storage.savePairs(pairs);
        UI.refreshPairsList();
      };

      document.getElementById('ttk-save-all').onclick = () => {
        UI.savePairs();
      };

      UI.refreshPairsList();
    },

    refreshPairsList: () => {
      const container = document.getElementById('ttk-pairs-container');
      if (!container) return;

      container.innerHTML = '';
      const pairs = Storage.getPairs();

      if (pairs.length === 0) {
        container.innerHTML = `
          <div style="text-align: center; padding: 20px; color: #a0a0a5;">
            No streamer pairs added yet. Click "Add Pair" to get started.
          </div>
        `;
        return;
      }

      pairs.forEach((pair, index) => {
        const pairElement = document.createElement('div');
        pairElement.className = 'ttk-pair';
        pairElement.innerHTML = `
          <div class="ttk-pair-header">
            <span class="ttk-pair-number">Pair #${index + 1}</span>
            <button class="ttk-delete-pair" data-index="${index}">Delete</button>
          </div>
          <div class="ttk-input-group">
            <label class="ttk-input-label">Twitch Username</label>
            <input type="text" class="ttk-input ttk-twitch-input" data-index="${index}" value="${pair.twitch || ''}">
          </div>
          <div class="ttk-input-group">
            <label class="ttk-input-label">Kick Username</label>
            <input type="text" class="ttk-input ttk-kick-input" data-index="${index}" value="${pair.kick || ''}">
          </div>
        `;
        container.appendChild(pairElement);
      });

      // Add event listeners to delete buttons
      document.querySelectorAll('.ttk-delete-pair').forEach(btn => {
        btn.onclick = (e) => {
          const index = parseInt(e.target.getAttribute('data-index'));
          const pairs = Storage.getPairs();
          pairs.splice(index, 1);
          Storage.savePairs(pairs);
          UI.refreshPairsList();
        };
      });
    },

    savePairs: () => {
      const twitchInputs = document.querySelectorAll('.ttk-twitch-input');
      const kickInputs = document.querySelectorAll('.ttk-kick-input');
      const pairs = [];

      for (let i = 0; i < twitchInputs.length; i++) {
        const twitch = twitchInputs[i].value.trim();
        const kick = kickInputs[i].value.trim();

        if (twitch || kick) {
          pairs.push({ twitch, kick });
        }
      }

      Storage.savePairs(pairs);
      UI.showNotification('Settings saved successfully!');
    },

    showNotification: (message, duration = 3000) => {
      // Remove any existing notification
      const existingNotification = document.querySelector('.ttk-notification');
      if (existingNotification) {
        existingNotification.remove();
      }

      const notification = document.createElement('div');
      notification.className = 'ttk-notification';
      notification.textContent = message;
      document.body.appendChild(notification);

      // Force reflow before adding the show class
      notification.offsetHeight;
      notification.classList.add('show');

      setTimeout(() => {
        notification.classList.remove('show');
        setTimeout(() => notification.remove(), 300);
      }, duration);
    }
  };

  // =================== INITIALIZATION ===================
  function initialize() {
    UI.init();

    // Set up auto-switch monitoring
    setInterval(() => {
      const currentChannel = StreamManager.getCurrentChannel();
      if (currentChannel) {
        StreamManager.autoSwitch();
      }
    }, CONFIG.checkInterval);

    // Initial check
    setTimeout(() => {
      StreamManager.autoSwitch();
    }, CONFIG.initDelay);
  }

  // Execute when DOM is fully loaded
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initialize);
  } else {
    initialize();
  }
})();