SOOP Auto Next

자동 다음 영상

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();