SOOP Auto Next

자동 다음 영상

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         SOOP Auto Next
// @namespace    https://vod.sooplive.com/
// @version      3.1
// @description  자동 다음 영상
// @author       you
// @match        https://vod.sooplive.com/player/*
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  /* =========================================================
     설정값 (Tampermonkey 저장소 사용 - 팝업이 없으므로
     설정 UI를 페이지 위에 iOS 스타일 패널로 직접 그려줍니다)
  ========================================================= */
  const DEFAULTS = { enabled: true, seconds: 60, endOnly: false };

  function getSettings() {
    return {
      enabled: GM_getValue("enabled", DEFAULTS.enabled),
      seconds: GM_getValue("seconds", DEFAULTS.seconds),
      endOnly: GM_getValue("endOnly", DEFAULTS.endOnly),
    };
  }

  function setSetting(key, value) {
    GM_setValue(key, value);
  }

  /* =========================================================
     스타일 (iOS 느낌: 라운드, 블러, SF 폰트, 파란/그린 포인트)
  ========================================================= */
  GM_addStyle(`
    #san-fab {
      position: fixed;
      right: 20px;
      bottom: 90px;
      width: 52px;
      height: 52px;
      border-radius: 50%;
      background: rgba(255,255,255,0.7);
      backdrop-filter: blur(20px) saturate(180%);
      -webkit-backdrop-filter: blur(20px) saturate(180%);
      box-shadow: 0 4px 18px rgba(0,0,0,0.25);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 22px;
      cursor: pointer;
      z-index: 999999;
      user-select: none;
      transition: transform 0.15s ease;
      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
    }
    #san-fab:active { transform: scale(0.9); }

    #san-backdrop {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.25);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.25s ease;
      z-index: 999998;
    }
    #san-backdrop.show { opacity: 1; pointer-events: auto; }

    #san-sheet {
      position: fixed;
      left: 50%;
      bottom: 0;
      width: min(92vw, 380px);
      transform: translate(-50%, 100%);
      transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
      z-index: 999999;
      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
      padding-bottom: 24px;
    }
    #san-sheet.show { transform: translate(-50%, 0); }

    .san-handle {
      width: 36px;
      height: 5px;
      border-radius: 999px;
      background: rgba(255,255,255,0.5);
      margin: 10px auto 8px;
    }

    .san-title {
      text-align: center;
      color: #fff;
      font-size: 13px;
      font-weight: 600;
      letter-spacing: 0.3px;
      margin-bottom: 10px;
      opacity: 0.85;
    }

    .san-group {
      background: rgba(30,30,32,0.72);
      backdrop-filter: blur(24px) saturate(180%);
      -webkit-backdrop-filter: blur(24px) saturate(180%);
      border-radius: 16px;
      margin: 0 14px 12px;
      overflow: hidden;
    }

    .san-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 13px 16px;
      border-bottom: 0.5px solid rgba(255,255,255,0.12);
    }
    .san-row:last-child { border-bottom: none; }

    .san-label {
      color: #fff;
      font-size: 15px;
      font-weight: 400;
    }
    .san-sub {
      color: rgba(255,255,255,0.5);
      font-size: 12px;
      margin-top: 2px;
    }

    .san-switch {
      position: relative;
      width: 51px;
      height: 31px;
      flex-shrink: 0;
    }
    .san-switch input { display: none; }
    .san-switch .san-slider {
      position: absolute;
      inset: 0;
      background: rgba(120,120,128,0.32);
      border-radius: 999px;
      transition: 0.2s;
    }
    .san-switch .san-slider::before {
      content: "";
      position: absolute;
      width: 27px;
      height: 27px;
      left: 2px;
      top: 2px;
      background: #fff;
      border-radius: 50%;
      box-shadow: 0 3px 8px rgba(0,0,0,0.2);
      transition: 0.2s;
    }
    .san-switch input:checked + .san-slider {
      background: #34c759;
    }
    .san-switch input:checked + .san-slider::before {
      transform: translateX(20px);
    }

    .san-slider-row {
      padding: 12px 16px 16px;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .san-slider-top {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
    }
    .san-value {
      color: #0a84ff;
      font-size: 15px;
      font-weight: 600;
    }
    input[type="range"].san-range {
      -webkit-appearance: none;
      width: 100%;
      height: 4px;
      border-radius: 999px;
      background: rgba(120,120,128,0.32);
      accent-color: #0a84ff;
    }
    input[type="range"].san-range::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 22px;
      height: 22px;
      border-radius: 50%;
      background: #fff;
      box-shadow: 0 2px 6px rgba(0,0,0,0.3);
      cursor: pointer;
    }

    /* 영상 위 진행 오버레이 (더 심플/미니멀하게) */
    #san-overlay {
      position: absolute;
      top: 14px;
      left: 50%;
      transform: translateX(-50%) scale(0.96);
      opacity: 0;
      width: 46%;
      min-width: 160px;
      z-index: 9999;
      pointer-events: none;
      transition: opacity 0.25s ease, transform 0.25s ease;
      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
    }
    #san-overlay.show {
      opacity: 1;
      transform: translateX(-50%) scale(1);
    }
    #san-overlay-inner {
      background: rgba(28,28,30,0.6);
      backdrop-filter: blur(16px) saturate(180%);
      -webkit-backdrop-filter: blur(16px) saturate(180%);
      padding: 9px 12px;
      border-radius: 14px;
    }
    #san-progress-track {
      height: 4px;
      background: rgba(255,255,255,0.25);
      border-radius: 999px;
      overflow: hidden;
    }
    #san-progress-fill {
      height: 100%;
      width: 100%;
      border-radius: 999px;
      background: #0a84ff;
      transition: width 1s linear;
    }
    #san-time-text {
      text-align: center;
      font-size: 11px;
      margin-top: 5px;
      color: rgba(255,255,255,0.85);
      font-weight: 500;
      letter-spacing: 0.2px;
    }
  `);

  /* =========================================================
     iOS 스타일 설정 패널 (플로팅 버튼 -> 바텀시트)
  ========================================================= */
  function buildSettingsUI() {
    if (document.getElementById("san-fab")) return;

    const fab = document.createElement("div");
    fab.id = "san-fab";
    fab.textContent = "⏱";
    document.body.appendChild(fab);

    const backdrop = document.createElement("div");
    backdrop.id = "san-backdrop";
    document.body.appendChild(backdrop);

    const sheet = document.createElement("div");
    sheet.id = "san-sheet";
    sheet.innerHTML = `
      <div class="san-handle"></div>
      <div class="san-title">AUTO NEXT</div>

      <div class="san-group">
        <div class="san-row">
          <div>
            <div class="san-label">자동 실행</div>
          </div>
          <label class="san-switch">
            <input type="checkbox" id="san-enabled">
            <span class="san-slider"></span>
          </label>
        </div>
        <div class="san-row">
          <div>
            <div class="san-label">영상 종료 시에만 이동</div>
          </div>
          <label class="san-switch">
            <input type="checkbox" id="san-endonly">
            <span class="san-slider"></span>
          </label>
        </div>
      </div>

      <div class="san-group">
        <div class="san-slider-row">
          <div class="san-slider-top">
            <span class="san-label">대기 시간</span>
            <span class="san-value"><span id="san-seconds-val">60</span>초</span>
          </div>
          <input type="range" class="san-range" id="san-seconds" min="1" max="100">
        </div>
      </div>
    `;
    document.body.appendChild(sheet);

    const enabledEl = sheet.querySelector("#san-enabled");
    const endOnlyEl = sheet.querySelector("#san-endonly");
    const secondsEl = sheet.querySelector("#san-seconds");
    const secondsValEl = sheet.querySelector("#san-seconds-val");

    function refreshUI() {
      const s = getSettings();
      enabledEl.checked = s.enabled;
      endOnlyEl.checked = s.endOnly;
      secondsEl.value = s.seconds;
      secondsValEl.textContent = s.seconds;
    }
    refreshUI();

    function openSheet() {
      refreshUI();
      backdrop.classList.add("show");
      sheet.classList.add("show");
    }
    function closeSheet() {
      backdrop.classList.remove("show");
      sheet.classList.remove("show");
    }

    fab.addEventListener("click", () => {
      if (sheet.classList.contains("show")) closeSheet();
      else openSheet();
    });
    backdrop.addEventListener("click", closeSheet);

    enabledEl.addEventListener("change", () => {
      setSetting("enabled", enabledEl.checked);
      resetAll();
      const v = getVideo();
      if (v) startTimer(v);
    });
    endOnlyEl.addEventListener("change", () => {
      setSetting("endOnly", endOnlyEl.checked);
      resetAll();
      const v = getVideo();
      if (v) startTimer(v);
    });
    secondsEl.addEventListener("input", () => {
      const val = parseInt(secondsEl.value, 10);
      secondsValEl.textContent = val;
      setSetting("seconds", val);
    });
    secondsEl.addEventListener("change", () => {
      resetAll();
      const v = getVideo();
      if (v) startTimer(v);
    });
  }

  /* =========================================================
     자동 다음영상 핵심 로직 (기존 content.js 로직 이식)
  ========================================================= */
  let timer = null;
  let interval = null;
  let overlay = null;
  let currentVideo = null;

  let startTime = 0;
  let duration = 0;
  let isRunning = false;

  function getVideo() {
    return document.querySelector("#video");
  }

  function goNextVideo() {
    const nextBtn = document.querySelector(".play.next");
    if (nextBtn) {
      document.getElementById("player")?.dispatchEvent(
        new MouseEvent("mousemove", { bubbles: true })
      );
      setTimeout(() => {
        try {
          nextBtn.click();
        } catch {}
      }, 200);
    }
  }

  function resetAll() {
    if (timer) clearTimeout(timer);
    if (interval) clearInterval(interval);

    timer = null;
    interval = null;

    startTime = 0;
    isRunning = false;

    if (overlay) {
      overlay.remove();
      overlay = null;
    }
  }

  function waitForPlayer(callback) {
    const check = setInterval(() => {
      const player = document.getElementById("player");
      if (player) {
        clearInterval(check);
        callback(player);
      }
    }, 200);
  }

  function createOverlay() {
    if (overlay) return;

    const player = document.getElementById("player");
    if (!player) {
      waitForPlayer(() => createOverlay());
      return;
    }

    overlay = document.createElement("div");
    overlay.id = "san-overlay";
    overlay.innerHTML = `
      <div id="san-overlay-inner">
        <div id="san-progress-track">
          <div id="san-progress-fill"></div>
        </div>
        <div id="san-time-text">60s</div>
      </div>
    `;
    player.appendChild(overlay);

    requestAnimationFrame(() => {
      overlay.classList.add("show");
    });
  }

  function startProgress() {
    if (interval) clearInterval(interval);

    interval = setInterval(() => {
      const elapsed = Math.floor((Date.now() - startTime) / 1000);
      const remaining = duration - elapsed;

      if (remaining <= 0) {
        clearInterval(interval);
        goNextVideo();
        return;
      }

      const fill = document.getElementById("san-progress-fill");
      const text = document.getElementById("san-time-text");

      if (fill) fill.style.width = (remaining / duration) * 100 + "%";
      if (text) text.textContent = remaining + "s";
    }, 1000);
  }

  function startTimer(video) {
    const { enabled, seconds, endOnly } = getSettings();

    if (!enabled || endOnly) {
      resetAll();
      return;
    }

    if (isRunning) return;

    isRunning = true;
    duration = seconds;

    if (!startTime) {
      startTime = Date.now();
    }

    createOverlay();
    startProgress();

    video.addEventListener("ended", () => {
      const s = getSettings();
      if (s.enabled && s.endOnly) goNextVideo();
    });
  }

  function watchVideoChange() {
    const observer = new MutationObserver(() => {
      const video = getVideo();

      if (video && video !== currentVideo) {
        resetAll();
        currentVideo = video;

        startTimer(video);

        video.addEventListener("play", () => {
          startTimer(video);
        });
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  function init() {
    buildSettingsUI();
    watchVideoChange();

    const video = getVideo();
    if (video) {
      currentVideo = video;
      startTimer(video);

      video.addEventListener("play", () => {
        startTimer(video);
      });
    }
  }

  init();
})();