WCO Auto-Play Next/Random with Dice (v1.4 — wcostream fixed)

Original panel/logic; fixed autoplay & random for wcostream.tv; resilient to ads

// ==UserScript==
// @name         WCO Auto-Play Next/Random with Dice (v1.4 — wcostream fixed)
// @namespace    http://tampermonkey.net/
// @author       P3k
// @version      1.4
// @license      GNU GENERAL PUBLIC LICENSE
// @description  Original panel/logic; fixed autoplay & random for wcostream.tv; resilient to ads
// @match        *://wcostream.tv/*
// @match        *://www.wcostream.tv/*
// @match        *://embed.wcostream.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // -----------------------------------------------------------------------------
  // 1) STUBS (unchanged)
  // -----------------------------------------------------------------------------
  window.downloadJSAtOnload = () => {};
  window.sub = () => {};

  // -----------------------------------------------------------------------------
  // 2) IFRAME CONTEXT (now ALWAYS hooks "ended"; autoplay click only if requested)
  // -----------------------------------------------------------------------------
  if (window.top !== window.self) {
    // Always hook ended so parent can decide next/random regardless of autoplay click
    const endedObs = new MutationObserver((_, obs) => {
      const v = document.querySelector('video');
      if (v) {
        try { v.muted = false; } catch {}
        v.addEventListener('ended', () => {
          window.parent.postMessage({ type: 'WCO_VIDEO_ENDED' }, '*');
        });
        obs.disconnect();
      }
    });
    endedObs.observe(document.documentElement, { childList: true, subtree: true });

    // Only auto-click big play when parent tells us autoplay is enabled
    window.addEventListener('message', (event) => {
      if (event.data?.type === 'WCO_AUTOPLAY_PREF' && event.data.autoplay) {
        const playObs = new MutationObserver((_, obs) => {
          const btn = document.querySelector('button.vjs-big-play-button');
          if (btn) { btn.click(); obs.disconnect(); }
        });
        playObs.observe(document.documentElement, { childList: true, subtree: true });
      }
    });
    return;
  }

  // -----------------------------------------------------------------------------
  // 3) PARENT PAGE CONTEXT
  // -----------------------------------------------------------------------------
  const episodes = []; // random + fallback next use this

  function abs(href, base) { try { return new URL(href, base || location.origin).href; } catch { return null; } }
  function samePath(a, b) {
    try {
      const A = new URL(a, location.origin);
      const B = new URL(b, location.origin);
      return A.pathname.replace(/\/+$/,'') === B.pathname.replace(/\/+$/,'');
    } catch { return a === b; }
  }
  function pushEpisode(url, title) {
    if (!url) return;
    if (!episodes.some(e => samePath(e.url, url))) episodes.push({ url, title: title || url });
  }

  // Collect from THIS page immediately (sidebar + main list if present)
  function collectFromCurrentPage(doc = document, base) {
    // Main list (if on the show page like the HTML you pasted)
    doc.querySelectorAll('#catlist-listview li a, #catlist-listview a.sonra').forEach(a => {
      pushEpisode(abs(a.getAttribute('href'), base), a.textContent.trim());
    });
    // Sidebar mirror (present on episode pages)
    doc.querySelectorAll('#sidebar .menustyle a, #sidebar .menusttyle a').forEach(a => {
      pushEpisode(abs(a.getAttribute('href'), base), a.textContent.trim());
    });
  }

  // Also try fetching the canonical show page to be thorough
  function deriveSlugFromLocation() {
    const seg = location.pathname.replace(/\/+$/,'').split('/')[1] || '';
    const m = seg.match(/^(.+?)-(?:season|episode|special|ova)\b/i);
    return (m && m[1]) ? m[1] : seg || null;
  }
  function buildShowCandidates() {
    const out = new Set();
    document.querySelectorAll('a[href^="/playlist-cat/"]').forEach(a => {
      const slug = (a.getAttribute('href') || '').split('/').pop();
      if (slug) { out.add(abs('/' + slug)); out.add(abs('/anime/' + slug)); }
    });
    const slug = deriveSlugFromLocation();
    if (slug) { out.add(abs('/' + slug)); out.add(abs('/anime/' + slug)); }
    return Array.from(out);
  }
  function parseShowHTML(html, base) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    collectFromCurrentPage(doc, base);
  }
  async function loadShowEpisodes() {
    const candidates = buildShowCandidates();
    for (const url of candidates) {
      try {
        const res = await fetch(url, { credentials: 'include' });
        if (!res.ok) continue;
        const html = await res.text();
        if (!/#catlist-listview/.test(html)) continue; // must look like show page
        parseShowHTML(html, url);
        if (episodes.length) return true;
      } catch {}
    }
    return false;
  }

  // -----------------------------------------------------------------------------
  // 4) ORIGINAL PANEL (exact layout; layered above ads)
  // -----------------------------------------------------------------------------
  function makePanel(iframe) {
    const storedNext = localStorage.getItem('wco-auto-next');
    const storedRand = localStorage.getItem('wco-auto-random');
    const storedAuto = localStorage.getItem('wco-auto-play');
    const defaultNext = (storedNext === null && storedRand === null) ? true : (storedNext === 'true');
    const defaultRand = (storedRand === 'true');
    const defaultAuto = (storedAuto === null) ? true : (storedAuto === 'true');

    const outer = document.createElement('div');
    outer.style.cssText = `
      display:flex;flex-direction:row;justify-content:center;gap:16px;margin:12px auto;
      font-family:sans-serif;font-size:14px;background:transparent;
      position:relative;z-index:2147483647;pointer-events:auto;
    `;

    function makeToggle(id, labelText, isChecked) {
      const label = document.createElement('label');
      label.style.cssText = 'display:flex;align-items:center;cursor:pointer;white-space:nowrap;';
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.id = id;
      checkbox.checked = isChecked;
      checkbox.style.marginRight = '6px';
      label.append(checkbox, document.createTextNode(labelText));
      return { label, checkbox };
    }

    // Section 1: Next Episode
    const section1 = document.createElement('div');
    section1.style.cssText = 'border:1px solid #aaa;padding:8px;background:#fff;min-width:220px;';
    const title1 = document.createElement('div');
    title1.textContent = 'Next Episode';
    title1.style.cssText = 'font-weight:bold;margin-bottom:6px;text-align:center;';
    const toggles = document.createElement('div');
    toggles.style.cssText = 'display:flex;flex-direction:row;gap:16px;justify-content:center;';
    const nextToggle = makeToggle('wco-auto-next', 'Sequential', defaultNext);
    const randToggle = makeToggle('wco-auto-random', 'Random', defaultRand);
    nextToggle.checkbox.addEventListener('change', () => {
      if (nextToggle.checkbox.checked) randToggle.checkbox.checked = false;
      savePrefs();
    });
    randToggle.checkbox.addEventListener('change', () => {
      if (randToggle.checkbox.checked) nextToggle.checkbox.checked = false;
      savePrefs();
    });
    toggles.append(nextToggle.label, randToggle.label);
    section1.append(title1, toggles);

    // Dice
    const diceButton = document.createElement('button');
    diceButton.innerHTML = '🎲';
    diceButton.style.cssText = `
      font-size:32px;width:50px;height:50px;border:1px solid #aaa;background:#fff;
      cursor:pointer;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:transform .2s;
    `;
    diceButton.title = 'Play Random Episode';
    diceButton.addEventListener('mouseenter', () => { diceButton.style.transform = 'scale(1.1)'; });
    diceButton.addEventListener('mouseleave', () => { diceButton.style.transform = 'scale(1)'; });
    diceButton.addEventListener('click', () => {
      if (episodes.length) {
        const pick = episodes[Math.floor(Math.random() * episodes.length)];
        if (pick?.url) location.href = pick.url;
      } else {
        console.log('WCO: Episode list not loaded yet');
      }
    });

    // Section 2: Auto Play
    const section2 = document.createElement('div');
    section2.style.cssText = 'border:1px solid #aaa;padding:8px;background:#fff;min-width:140px;';
    const title2 = document.createElement('div');
    title2.textContent = 'Auto Play';
    title2.style.cssText = 'font-weight:bold;margin-bottom:6px;text-align:center;';
    const box2 = document.createElement('div');
    box2.style.cssText = 'display:flex;justify-content:center;';
    const autoToggle = makeToggle('wco-auto-play', 'Enabled', defaultAuto);
    autoToggle.checkbox.addEventListener('change', () => {
      savePrefs();
      postAutoplay(); // update iframe immediately
    });
    box2.appendChild(autoToggle.label);
    section2.append(title2, box2);

    // Assemble in original order: AutoPlay | 🎲 | Next
    outer.append(section2, diceButton, section1);
    iframe.parentNode.insertBefore(outer, iframe.nextSibling);

    // Persist
    function savePrefs() {
      localStorage.setItem('wco-auto-next', nextToggle.checkbox.checked);
      localStorage.setItem('wco-auto-random', randToggle.checkbox.checked);
      localStorage.setItem('wco-auto-play', autoToggle.checkbox.checked);
    }

    // Push autoplay state to iframe (immediately + a few retries in case it was already loaded)
    function postAutoplay() {
      try {
        iframe.contentWindow.postMessage({ type: 'WCO_AUTOPLAY_PREF', autoplay: autoToggle.checkbox.checked }, '*');
      } catch {}
    }
    // Send now, on load, and ping a few times to guarantee the embed sees it
    postAutoplay();
    iframe.addEventListener('load', postAutoplay);
    const ping = setInterval(postAutoplay, 1500);
    setTimeout(() => clearInterval(ping), 8000); // stop after a few tries

    savePrefs();
  }

  // -----------------------------------------------------------------------------
  // 5) Handle video ended -> next/random (same behavior; better matching)
  // -----------------------------------------------------------------------------
  window.addEventListener('message', (event) => {
    if (event.data?.type !== 'WCO_VIDEO_ENDED') return;

    const rand = localStorage.getItem('wco-auto-random') === 'true';
    const next = (localStorage.getItem('wco-auto-next') === 'true') ||
                 (localStorage.getItem('wco-auto-next') === null && !rand); // default Next on first run

    if (rand && episodes.length) {
      const pick = episodes[Math.floor(Math.random() * episodes.length)];
      if (pick?.url) location.href = pick.url;
      return;
    }

    if (next) {
      const rel = document.querySelector('a[rel="next"]');
      if (rel?.href) { location.href = rel.href; return; }

      // Fallback: use our list (order on site is newest->oldest; "next" goes forward in list)
      if (episodes.length) {
        const idx = episodes.findIndex(e => samePath(e.url, location.href));
        if (idx >= 0 && idx + 1 < episodes.length) {
          location.href = episodes[idx + 1].url;
        }
      }
    }
  });

  // -----------------------------------------------------------------------------
  // 6) Install panel when the player iframe shows up (ads-safe), and build episode list
  // -----------------------------------------------------------------------------
  function isPlayerIframe(node) {
    if (!(node instanceof HTMLIFrameElement)) return false;
    const src = (node.getAttribute('src') || '').toLowerCase();
    return src.includes('embed.wcostream.com') || src.includes('/inc/embed/video-js.php');
  }
  function findPlayerIframe() {
    return Array.from(document.querySelectorAll('iframe')).find(isPlayerIframe) || null;
  }
  function installWhenIframeAppears() {
    const fr = findPlayerIframe();
    if (fr) { makePanel(fr); return; }
    const mo = new MutationObserver(() => {
      const i = findPlayerIframe();
      if (i) { makePanel(i); mo.disconnect(); }
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  }

  // Build episodes immediately from this page, then try fetching the show page too
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      collectFromCurrentPage();     // sidebar list available on episode pages
      loadShowEpisodes();           // enhances the list if needed
    });
  } else {
    collectFromCurrentPage();
    loadShowEpisodes();
  }

  if (document.readyState === 'complete') installWhenIframeAppears();
  else window.addEventListener('load', installWhenIframeAppears);
})();