Aternos Auto Start & Extend

Auto-start when offline; auto-extend

当前为 2025-09-26 提交的版本,查看 最新版本

// ==UserScript==
// @name         Aternos Auto Start & Extend
// @namespace    https://aternos.org/
// @version      1.0.0
// @description  Auto-start when offline; auto-extend
// @match        https://aternos.org/server
// @match        https://aternos.org/server/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const SELECTORS = {
    offlineContainer: 'div.status.offline',
    onlineContainer: 'div.status.online',
    startButton: '#start',
    players: '.live-status-box-value.js-players',
    countdown: '.server-end-countdown',
    extendButton: 'button.btn.btn-tiny.btn-success.server-extend-end',
  };

  let lastStartClickMs = 0;
  let lastExtendClickMs = 0;

  function isVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
  }

  function queryText(selector) {
    const el = document.querySelector(selector);
    return el ? el.textContent.trim() : '';
  }

  function isOffline() {
    return !!document.querySelector(SELECTORS.offlineContainer);
  }

  function isOnline() {
    return !!document.querySelector(SELECTORS.onlineContainer);
  }

  function playersAreZero() {
    const txt = queryText(SELECTORS.players); // e.g., "0/20"
    return /^0\s*\/\s*\d+$/i.test(txt);
  }

  function getCountdownSeconds() {
    const txt = queryText(SELECTORS.countdown); // e.g., "5:22" or "59s"
    if (!txt) return null;

    const trimmed = txt.replace(/\s+/g, '');
    if (/^\d{1,2}:\d{2}$/.test(trimmed)) {
      const [m, s] = trimmed.split(':').map(Number);
      if (Number.isFinite(m) && Number.isFinite(s)) return m * 60 + s;
    } else if (/^\d{1,3}s$/i.test(trimmed)) {
      const s = Number(trimmed.slice(0, -1));
      if (Number.isFinite(s)) return s;
    }
    return null;
  }

  function clickIfAvailable(selector, lastClickRef, minIntervalMs) {
    const now = Date.now();
    if (now - lastClickRef.value < minIntervalMs) return false;
    const btn = document.querySelector(selector);
    if (!btn || !isVisible(btn) || btn.disabled) return false;
    btn.click();
    lastClickRef.value = now;
    return true;
  }

  function tryStartIfOffline() {
    if (!isOffline()) return;
    clickIfAvailable(SELECTORS.startButton, { get value() { return lastStartClickMs; }, set value(v) { lastStartClickMs = v; } }, 10000);
  }

  function tryExtendIfEndingSoon() {
    if (!isOnline()) return;
    if (!playersAreZero()) return;

    const seconds = getCountdownSeconds();
    if (seconds === null) return;

    // If <= 59 seconds remaining, extend
    if (seconds <= 59) {
      clickIfAvailable(SELECTORS.extendButton, { get value() { return lastExtendClickMs; }, set value(v) { lastExtendClickMs = v; } }, 5000);
    }
  }

  // Initial small delay to allow SPA content to render
  setTimeout(() => {
    // Poll for offline -> start
    setInterval(tryStartIfOffline, 5000);
    // Poll for extend condition every second
    setInterval(tryExtendIfEndingSoon, 1000);

    // React to dynamic DOM changes faster
    const observer = new MutationObserver(() => {
      tryStartIfOffline();
      tryExtendIfEndingSoon();
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });
  }, 1500);
})();