WCO/WCOStream Auto-Play Next/Random (v1.5)

Works on both wco.tv and wcostream.tv layouts. Pinned, centred panel above the player.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WCO/WCOStream Auto-Play Next/Random (v1.5)
// @namespace    http://tampermonkey.net/
// @version      1.5
// @license      GNU GENERAL PUBLIC LICENSE
// @description  Works on both wco.tv and wcostream.tv layouts. Pinned, centred panel above the player.
// @match        *://www.wco.tv/*
// @match        *://wco.tv/*
// @match        *://www.wcostream.tv/*
// @match        *://wcostream.tv/*
// @match        *://embed.wcostream.com/*
// @grant        none
// @run-at       document-start
// @SOME UBLOCK FILTERS YOU SHOULD ADD:
// @!Watch Cartoons Online https://www.wcostream.com
// @wcostream.com##+js(rmnt, script, /embed.html)
// @wco.tv##+js(rmnt, script, /embed.html)
// @wcostream.com##.announcement-backdrop, #announcement
// @wco.tv##.announcement-backdrop, #announcement
// @||embed.wcostream.com/inc/embed/index.php?file=$frame,uritransform=/index/video-js/
// ==/UserScript==

(() => {
  'use strict';

  const MAX_ATTEMPTS = 120;
  const RETRY_MS = 150;
  const LS_NEXT   = 'wco-auto-next';
  const LS_RANDOM = 'wco-auto-random';

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  const q  = (sel, root=document) => root.querySelector(sel);
  const qa = (sel, root=document) => Array.from(root.querySelectorAll(sel));
  const visible = (el) => {
    if (!el) return false;
    const s = getComputedStyle(el);
    if (s.display === 'none' || s.visibility === 'hidden' || +s.opacity === 0) return false;
    const r = el.getBoundingClientRect();
    return r.width > 0 && r.height > 0;
  };
  const isPlaying = (v) => v && !v.paused && !v.ended && v.readyState >= 2;
  const norm = (u) => { try { return new URL(u, location.href).href.replace(/\/+$/,''); } catch { return (u||'').replace(/\/+$/,''); } };
  const sameURL = (a,b) => norm(a) === norm(b);
  const goTo = (href) => { if (href) location.href = href; };

  // ---------- unmute helpers ----------
  const hardUnmute = (v) => { if (!v) return; try { v.muted = false; } catch {} try { v.volume = Math.max(0.7, v.volume || 0.7); } catch {} };
  const unmuteViaVJS = (container) => {
    const vjs = window.videojs || window.videoJS || window.videoJs; if (!vjs) return false;
    try {
      let player = null;
      if (container?.id) { try { player = vjs(container.id); } catch {} }
      if (!player && typeof vjs.getPlayers === 'function') {
        const reg = vjs.getPlayers(); const ids = reg ? Object.keys(reg) : [];
        if (ids.length) player = reg[ids[0]];
      }
      if (player) { try { player.muted(false); } catch {} try { player.volume(0.7); } catch {} return true; }
    } catch {}
    return false;
  };
  const attachOneTimeUnmuteHandlers = (cb) => {
    let done = false;
    const fire = () => { if (done) return; done = true; off(); try { cb(); } catch {} };
    const types = ['pointerdown','mousedown','touchstart','keydown','click'];
    const on = () => types.forEach(t => window.addEventListener(t, fire, { passive: true, once: true, capture: true }));
    const off = () => types.forEach(t => window.removeEventListener(t, fire, { capture: true }));
    on();
  };

  // ---------- IFRAME (embed.wcostream.com) ----------
  if (location.hostname.replace(/^www\./,'') === 'embed.wcostream.com') {
    let started = false;
    const start = async () => {
      if (started) return;
      let v = null;
      for (let i=0; i<100 && !v; i++) { v = q('video'); if (!v) await sleep(50); }
      if (!v) return;
      try {
        v.setAttribute('playsinline',''); v.setAttribute('webkit-playsinline','');
        v.autoplay = true; v.muted = true;
        if (v.getAttribute('preload') === 'none') v.setAttribute('preload','metadata');
      } catch {}
      const markPlaying = () => { started = true; v.removeEventListener('playing', markPlaying); };
      v.addEventListener('playing', markPlaying, { once: true });
      if (!v.__wcoEndedHooked) {
        v.addEventListener('ended', () => { try { parent.postMessage({ type: 'WCO_VIDEO_ENDED' }, '*'); } catch {} }, { once: true });
        v.__wcoEndedHooked = true;
      }
      const tryImmediateUnmute = () => { hardUnmute(v); unmuteViaVJS(document); };
      v.addEventListener('playing', () => setTimeout(tryImmediateUnmute, 50), { once: true });
      attachOneTimeUnmuteHandlers(() => { tryImmediateUnmute(); try { v.play?.(); } catch {} });
      const clickBigPlay = () => { const btn = q('.vjs-big-play-button'); if (btn && visible(btn)) { try { btn.click(); } catch {} } };
      for (let i=0; i<MAX_ATTEMPTS && !isPlaying(v); i++) {
        try { await v.play(); } catch {}
        if (!isPlaying(v)) clickBigPlay();
        await sleep(RETRY_MS);
      }
    };
    const boot = () => {
      start();
      new MutationObserver(() => { if (!started) start(); })
        .observe(document.documentElement, { childList: true, subtree: true });
      document.addEventListener('visibilitychange', () => { if (!document.hidden && !started) start(); });
    };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
    else boot();
    return;
  }

  // ---------- PARENT ----------
  const episodes = [];
  let episodesPromise = null;

  // find candidate links on-page (wcostream sidebar "Episode List", and any obvious episode anchors)
  const scrapeEpisodesFromPage = () => {
    let added = 0;
    // 1) wcostream.tv sidebar Episode List
    qa('#sidebar .menu .menustyle ul li a[href]').forEach(a => {
      const url = a.href;
      if (url && !episodes.some(e => sameURL(e.url, url))) { episodes.push({ url, title: (a.textContent||'').trim() }); added++; }
    });
    // 2) any bookmark episode links on-page
    qa('a[rel="bookmark"][href]').forEach(a => {
      const url = a.href;
      if (url && /\/(episode|season|special)/i.test(url) && !episodes.some(e => sameURL(e.url,url))) {
        episodes.push({ url, title: (a.textContent||'').trim() }); added++;
      }
    });
    // 3) older series sidebar used on some pages
    qa('#sidebar_right3 .cat-eps a[href]').forEach(a => {
      const url = a.href;
      if (url && !episodes.some(e => sameURL(e.url, url))) { episodes.push({ url, title: (a.textContent||'').trim() }); added++; }
    });
    return added;
  };

  // try to fetch the series page and scrape its sidebar
  const fetchEpisodesFromSeriesPage = () => {
    if (episodesPromise) return episodesPromise;
    const categoryLink =
      q('a[rel="category tag"][href*="/anime/"]') ||
      q('a[href*="/anime/"][rel="category tag"]');
    if (!categoryLink) return Promise.resolve(0);

    episodesPromise = fetch(categoryLink.href)
      .then(r => r.text())
      .then(html => {
        const doc = new DOMParser().parseFromString(html, 'text/html');
        let added = 0;
        doc.querySelectorAll('#sidebar .menu .menustyle ul li a[href]').forEach(a => {
          const url = a.href;
          if (url && !episodes.some(e => sameURL(e.url,url))) { episodes.push({ url, title: (a.textContent||'').trim() }); added++; }
        });
        doc.querySelectorAll('#sidebar_right3 .cat-eps a[href]').forEach(a => {
          const url = a.href;
          if (url && !episodes.some(e => sameURL(e.url,url))) { episodes.push({ url, title: (a.textContent||'').trim() }); added++; }
        });
        return added;
      })
      .catch(() => 0);
    return episodesPromise;
  };

  // wait helper for episodes to be available (tries scraping and fetching)
  const ensureEpisodesReady = async (timeoutMs = 4000) => {
    // first, scrape what we can synchronously
    let count = scrapeEpisodesFromPage();

    // kick off fetch if still light
    if (episodes.length < 2) fetchEpisodesFromSeriesPage();

    const start = Date.now();
    while (episodes.length < 1 && Date.now() - start < timeoutMs) {
      await sleep(100);
      // try scraping again in case sidebar arrived late
      count += scrapeEpisodesFromPage();
    }
    return episodes.length;
  };

  const pickRandomDifferentFromCurrent = () => {
    const cur = norm(location.href);
    const pool = episodes.filter(e => !sameURL(e.url, cur));
    if (!pool.length) return null;
    return pool[Math.floor(Math.random() * pool.length)];
  };

  // smarter sequential for both sites
  const goNext = () => {
    let a = q('a[rel="next"]');
    if (a?.href) return goTo(a.href);
    a = q('a[rel="prev"]');
    if (a?.href) return goTo(a.href);
    if (episodes.length) {
      const cur = location.href;
      const idx = episodes.findIndex(e => sameURL(e.url, cur));
      const target = (idx >= 0 && idx+1 < episodes.length) ? episodes[idx+1] : episodes[0];
      if (target?.url && !sameURL(target.url, cur)) return goTo(target.url);
    }
  };

  const ensureIframeAutoplayAllowed = () => {
    qa('iframe#cizgi-js-0, .pcat-jwplayer iframe, iframe[src*="embed.wcostream.com"], iframe[data-type="wco-embed"]').forEach(ifr => {
      try {
        const cur = (ifr.getAttribute('allow')||'').toLowerCase();
        if (!cur.includes('autoplay')) ifr.setAttribute('allow', `${cur} autoplay; fullscreen`.trim());
      } catch {}
    });
  };

  const wireParentUnmuteForwarder = () => {
    attachOneTimeUnmuteHandlers(() => {
      qa('iframe#cizgi-js-0, .pcat-jwplayer iframe, iframe[src*="embed.wcostream.com"], iframe[data-type="wco-embed"]').forEach(ifr => {
        try { ifr.contentWindow?.postMessage({ type: 'WCO_UNMUTE' }, '*'); } catch {}
      });
      tryInlineUnmute();
    });
  };

  let inlineStarted = false;
  const tryInlineUnmute = () => {
    const container = qa('#video-js,.video-js').find(visible) || q('#video-js,.video-js') || document;
    const v = q('video.vjs-tech', container) || q('.video-js video', container) || q('video');
    if (v) { hardUnmute(v); unmuteViaVJS(container); try { v.play?.(); } catch {} }
  };

  const startInlineVideoJS = async () => {
    if (inlineStarted) return true;
    const container = qa('#video-js,.video-js').find(visible) || q('#video-js,.video-js') || document;
    const vids = [...qa('video.vjs-tech', container), ...qa('.video-js video', container), ...qa('video')];
    const v = vids.find(visible) || vids[0];
    if (!v) return false;

    if (!v.__wcoEndedHooked) {
      v.addEventListener('ended', () => { handleEnded(); }, { once: true });
      v.__wcoEndedHooked = true;
    }

    try {
      v.setAttribute('playsinline',''); v.setAttribute('webkit-playsinline','');
      v.autoplay = true; v.muted = true;
      if (v.getAttribute('preload') === 'none') v.setAttribute('preload','metadata');
    } catch {}

    const mark = () => { inlineStarted = true; v.removeEventListener('playing', mark); };
    v.addEventListener('playing', () => { mark(); setTimeout(() => { hardUnmute(v); unmuteViaVJS(container); }, 50); }, { once: true });

    const clickBigPlay = () => { const btn = container.querySelector('.vjs-big-play-button'); if (btn && visible(btn)) { try { btn.click(); } catch {} } };

    let usedAPI = false;
    const vjs = window.videojs || window.videoJS || window.videoJs;
    if (typeof vjs === 'function') {
      try {
        let player = null;
        if (container.id) { try { player = vjs(container.id); } catch {} }
        if (!player && typeof vjs.getPlayers === 'function') {
          const reg = vjs.getPlayers(); const ids = reg ? Object.keys(reg) : [];
          if (ids.length) player = reg[ids[0]];
        }
        if (player) {
          usedAPI = true;
          player.ready(async () => {
            try { player.muted(true); } catch {}
            try { player.autoplay(true); } catch {}
            for (let i=0; i<MAX_ATTEMPTS && !isPlaying(v); i++) {
              try { await player.play(); } catch {}
              if (!isPlaying(v)) clickBigPlay();
              await sleep(RETRY_MS);
            }
          });
        }
      } catch {}
    }

    if (!usedAPI) {
      for (let i=0; i<MAX_ATTEMPTS && !isPlaying(v); i++) {
        try { await v.play(); } catch {}
        if (!isPlaying(v)) clickBigPlay();
        await sleep(RETRY_MS);
      }
    }

    return inlineStarted || isPlaying(v);
  };

  // --------- prefs + ended decision ----------
  const nextOnDefault = () => {
    const n = localStorage.getItem(LS_NEXT);
    const r = localStorage.getItem(LS_RANDOM);
    return (n === null && r === null) ? true : (n === 'true');
  };
  const randOnDefault = () => localStorage.getItem(LS_RANDOM) === 'true';
  const setPrefs = (nextOn, randOn) => {
    localStorage.setItem(LS_NEXT, String(!!nextOn));
    localStorage.setItem(LS_RANDOM, String(!!randOn));
  };

  const handleEnded = () => {
    const randOn = localStorage.getItem(LS_RANDOM) === 'true';
    const nextOn = (localStorage.getItem(LS_NEXT) === 'true') ||
                   (localStorage.getItem(LS_NEXT) === null && localStorage.getItem(LS_RANDOM) === null);
    if (randOn && episodes.length) {
      const ep = pickRandomDifferentFromCurrent();
      if (ep?.url) { location.href = ep.url; return; }
    }
    if (nextOn) { goNext(); return; }
    goNext();
  };

  // --------- UI (centered card) ----------
  const injectCSS = () => {
    if (q('#wco-inline-panel-css')) return;
    const css = document.createElement('style');
    css.id = 'wco-inline-panel-css';
    css.textContent = `
      #wco-inline-panel{
        display:block; width:max-content; margin:10px auto 8px;
        background:#1e1f22; color:#fff; border:1px solid #2d2e33; border-radius:6px;
        padding:10px 12px; box-shadow:0 2px 8px rgba(0,0,0,.35);
        font:14px/1.25 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
        z-index:2147483647;
      }
      #wco-inline-panel .wco-title{font-weight:600;margin-bottom:6px;color:#e6e6e6;text-align:center}
      #wco-inline-panel .wco-row{display:flex;align-items:center;gap:16px;flex-wrap:wrap;justify-content:center}
      #wco-inline-panel input[type="checkbox"]{vertical-align:-2px;margin-right:6px}
      #wco-inline-panel .wco-dice{
        font-size:18px; width:36px; height:30px; border:1px solid #444; border-radius:6px;
        background:#2a2b30; color:#fff; cursor:pointer;
      }
      #wco-inline-panel .wco-dice:disabled{opacity:.5;cursor:not-allowed}
      #wco-inline-panel .wco-dice:active{transform:scale(.98)}
    `;
    document.head.appendChild(css);
  };

  const findAnchor = () => {
    let el = q('div[id^="hide-cizgi-video-"]'); if (el) return el;
    el = qa('iframe[src*="embed.wcostream.com"], iframe[data-type="wco-embed"]').find(visible); if (el) return el;
    el = qa('#video-js,.video-js, video').find(visible); if (el) return el;
    return qa('iframe').find(visible) || null;
  };

  const buildInlinePanel = () => {
    const anchor = findAnchor();
    if (!anchor || !anchor.parentElement) return;

    let panel = q('#wco-inline-panel');
    if (!panel) {
      panel = document.createElement('div'); panel.id = 'wco-inline-panel';
      const title = document.createElement('div'); title.className = 'wco-title'; title.textContent = 'Episode Advance'; panel.appendChild(title);

      const row = document.createElement('div'); row.className = 'wco-row'; panel.appendChild(row);

      const mkToggle = (id, label, checked) => {
        const wrap = document.createElement('label');
        const cb = document.createElement('input');
        cb.type = 'checkbox'; cb.id = id; cb.checked = !!checked;
        wrap.appendChild(cb); wrap.appendChild(document.createTextNode(' ' + label));
        return {wrap, cb};
      };

      const nextT = mkToggle('wco-next', 'Sequential', nextOnDefault());
      const randT = mkToggle('wco-rand', 'Random',     randOnDefault());
      row.appendChild(nextT.wrap); row.appendChild(randT.wrap);

      const dice = document.createElement('button');
      dice.type = 'button'; dice.className = 'wco-dice'; dice.textContent = '🎲'; dice.title = 'Play a random episode now';
      row.appendChild(dice);

      const sync = () => setPrefs(nextT.cb.checked, randT.cb.checked);
      nextT.cb.addEventListener('change', () => { if (nextT.cb.checked) randT.cb.checked = false; sync(); });
      randT.cb.addEventListener('change', () => { if (randT.cb.checked) nextT.cb.checked = false; sync(); });

      // 🔧 Robust RANDOM click
      dice.addEventListener('click', async () => {
        try {
          dice.disabled = true;
          // make sure we have something to choose from
          await ensureEpisodesReady(4000);

          // if still nothing, one last synchronous scrape (DOM might have changed)
          if (!episodes.length) scrapeEpisodesFromPage();

          const ep = pickRandomDifferentFromCurrent();
          if (ep?.url) location.href = ep.url;
          // if only one item and it's the current page, do nothing (no change)
        } finally {
          dice.disabled = false;
        }
      });
    }

    if (anchor.previousSibling !== panel) anchor.parentElement.insertBefore(panel, anchor);
  };

  // ---- early watcher
  (function startEarlyPanelWatcher(){
    injectCSS();
    let fastTries = 0;
    const fastLoop = () => {
      buildInlinePanel();
      if (q('#wco-inline-panel')) return;
      fastTries++; if (fastTries < 300) setTimeout(fastLoop, 50);
    };
    fastLoop();

    const mo = new MutationObserver(() => {
      if (!q('#wco-inline-panel')) buildInlinePanel();
      else {
        const anchor = findAnchor();
        if (anchor && q('#wco-inline-panel')?.nextSibling !== anchor) {
          try { anchor.parentElement.insertBefore(q('#wco-inline-panel'), anchor); } catch {}
        }
      }
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  })();

  // ---- boot
  const bootParent = async () => {
    window.addEventListener('message', (e) => { if (e?.data?.type === 'WCO_VIDEO_ENDED') handleEnded(); });
    ensureIframeAutoplayAllowed();
    wireParentUnmuteForwarder();

    // pre-warm episodes list in the background (non-blocking)
    scrapeEpisodesFromPage();
    fetchEpisodesFromSeriesPage();

    for (let i=0; i<Math.ceil(MAX_ATTEMPTS*1.2); i++) {
      ensureIframeAutoplayAllowed();
      const ok = await startInlineVideoJS();
      if (ok) break;
      await sleep(RETRY_MS);
    }

    document.addEventListener('visibilitychange', () => {
      if (!document.hidden && !inlineStarted) startInlineVideoJS();
    });
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', bootParent, { once: true });
  } else bootParent();

})();