SOOP Auto Next

자동 다음 영상

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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