Internet Roadtrip Permanent Radio - WBOR

Overrides Internet Roadtrip radio with WBOR and shows live song and show info

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Internet Roadtrip Permanent Radio - WBOR
// @description  Overrides Internet Roadtrip radio with WBOR and shows live song and show info
// @namespace    http://tampermonkey.net/
// @match        https://neal.fun/internet-roadtrip/
// @version      1.3.6
// @author       TotallyNotSamm
// @license      MIT
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/[email protected]
// @grant        GM_xmlhttpRequest
// @grant        GM.getValue
// @grant        GM.setValue
// @connect      azura.wbor.org
// @connect      playlists.wbor.org
// @icon        https://avatars.githubusercontent.com/u/165192665?s=280&v=4
// ==/UserScript==

(async function () {
  if (!IRF.isInternetRoadtrip) return;

  // Check if we've shown the migration alert before
  const hasShownMigrationAlert = await GM.getValue("hasShownMigrationAlert", false);
  if (!hasShownMigrationAlert) {
    alert("⚠️ Important Notice ⚠️\n\nThis radioscript will no longer receive updates.\n\nThe new version is available on GreasyFork. You can find it by:\n1. Opening the Updates tab in settings\n2. Checking pins in #mod-dev in the discord server.");
    await GM.setValue("hasShownMigrationAlert", true);
  }

  // Create the update notice tab first
  const updateTab = await IRF.ui.panel.createTabFor(GM.info, { tabName: "Updates" });
  const updateContainer = document.createElement("div");
  updateContainer.style.padding = "1rem";

  const warningDiv = document.createElement("div");
  warningDiv.style.backgroundColor = "#fff3cd";
  warningDiv.style.color = "#856404";
  warningDiv.style.padding = "1rem";
  warningDiv.style.borderRadius = "0.25rem";
  warningDiv.style.marginBottom = "1rem";
  warningDiv.style.border = "1px solid #ffeeba";

  const warningTitle = document.createElement("h3");
  warningTitle.textContent = "⚠️ Important: Script Migration Required";
  warningTitle.style.marginTop = "0";
  warningTitle.style.marginBottom = "0.5rem";

  const warningText = document.createElement("p");
  warningText.innerHTML = `This version of Internet Roadtrip WBOR Radio will no longer receive updates.<br><br>
                         The new version "Internet Roadtrip Permanent Radios" is available on GreasyFork.<br><br>
                         You can get the new version in one of two ways:`;
  warningText.style.marginBottom = "1rem";

  const optionsList = document.createElement("ul");
  optionsList.style.marginBottom = "1rem";
  optionsList.style.paddingLeft = "1.5rem";

  const option1 = document.createElement("li");
  option1.innerHTML = `Click this direct link to the new script on GreasyFork:<br>
                      <a href="https://greasyfork.org/en/scripts/538771-internet-roadtrip-permanent-radios" target="_blank" style="color: #004085; text-decoration: underline; word-break: break-all;">
                        https://greasyfork.org/en/scripts/538771-internet-roadtrip-permanent-radios
                      </a>`;
  option1.style.marginBottom = "0.5rem";

  const option2 = document.createElement("li");
  option2.innerHTML = `Look for <strong>"Internet Roadtrip Permanent Radios"</strong> in pins in #mod-dev`;

  optionsList.appendChild(option1);
  optionsList.appendChild(option2);

  warningDiv.appendChild(warningTitle);
  warningDiv.appendChild(warningText);
  warningDiv.appendChild(optionsList);
  updateContainer.appendChild(warningDiv);
  updateTab.container.appendChild(updateContainer);

  let nowPlayingText;
  let liveShowName = null;
  let liveDjName = null;
  let isPopupOpen = false;

  async function fetchNowPlaying() {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: "https://azura.wbor.org/api/nowplaying/1",
        onload: function (response) {
          try {
            const data = JSON.parse(response.responseText);
            const song = data[0]?.now_playing?.song || {};
            const artist = song.artist || "Unknown Artist";
            const title = song.title || "Unknown Title";
            nowPlayingText = `${title} – ${artist}`;
          } catch (e) {
            console.error("[WBOR] JSON parse error:", e);
            nowPlayingText = "Unknown Track – Unknown Artist";
          } finally {
            resolve();
          }
        },
        onerror: function (e) {
          console.error("[WBOR] GM_xmlhttpRequest failed:", e);
          nowPlayingText = "Unknown Track – Unknown Artist";
          resolve();
        }
      });
    });
  }

  async function fetchLiveShowInfo() {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: "https://playlists.wbor.org/WBOR/",
        onload: function (response) {
          try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(response.responseText, "text/html");

            const showTitleEl = doc.querySelector("h3.show-title a");
            liveShowName = showTitleEl ? showTitleEl.textContent.trim() : null;

            const djNameEl = doc.querySelector("p.dj-name a");
            liveDjName = djNameEl ? djNameEl.textContent.trim() : null;

          } catch (e) {
            console.error("[WBOR] Failed to parse live show info HTML:", e);
            liveShowName = null;
            liveDjName = null;
          } finally {
            resolve();
          }
        },
        onerror: function (e) {
          console.error("[WBOR] GM_xmlhttpRequest failed for live show info:", e);
          liveShowName = null;
          liveDjName = null;
          resolve();
        }
      });
    });
  }

  async function updateAllInfo() {
    await Promise.all([fetchNowPlaying(), fetchLiveShowInfo()]);
  }
  await updateAllInfo();
  setInterval(updateAllInfo, 30000);

  const container = await IRF.vdom.container;
  const originalUpdateData = container.methods.updateData;

  container.state.updateData = new Proxy(originalUpdateData, {
    apply: (target, thisArg, args) => {
      const currentStation = args[0].station?.name;
      const alreadySet = currentStation === "WBOR 91.1 FM";

      if (!alreadySet) {
        args[0].station = {
          name: "WBOR 91.1 FM",
          url: "https://listen.wbor.org/",
          distance: 0
        };
      }

      IRF.vdom.radio.then(radio => {
        if (radio.state.isPoweredOn) {
          radio.state.stationInfo = nowPlayingText;
        } else {
          radio.state.stationInfo = "TUNE IN";
        }
      });

      return Reflect.apply(target, thisArg, args);
    }
  });

const radioBody = document.querySelector(".radio-body");
if (!radioBody) {
  console.warn("[WBOR] .radio-body not found. The info button won't be displayed.");
  return;
}

radioBody.style.position = "relative";

  const infoButton = document.createElement("button");
  infoButton.textContent = "i";
  infoButton.setAttribute("aria-label", "Show WBOR Info");
  Object.assign(infoButton.style, {
    position: "absolute",
    top: "8px",
    left: "8px",
    width: "20px",
    height: "20px",
    borderRadius: "50%",
    border: "none",
    background: "transparent",
    color: "white",
    fontWeight: "bold",
    fontFamily: "inherit",
    cursor: "pointer",
    padding: "0",
    lineHeight: "18px",
    textAlign: "center",
    userSelect: "none",
    zIndex: "9999",
  });
  radioBody.appendChild(infoButton);

  const tooltipSpan = document.createElement("div");
  tooltipSpan.textContent = "Show more info";
  Object.assign(tooltipSpan.style, {
    position: "fixed",
    backgroundColor: "rgba(0, 0, 0, 0.75)",
    color: "#fff",
    padding: "4px 8px",
    borderRadius: "4px",
    fontSize: "12px",
    fontFamily: "inherit",
    opacity: "0",
    visibility: "hidden",
    transition: "opacity 0.2s ease",
    whiteSpace: "nowrap",
    zIndex: "9998",
  });
  document.body.appendChild(tooltipSpan);

  const infoPopup = document.createElement("div");
  const refStyles = getComputedStyle(radioBody.querySelector(".station-name"));
  Object.assign(infoPopup.style, {
    position: "fixed",
    backgroundColor: "rgba(0, 0, 0, 0.75)",
    color: "#fff",
    padding: "8px 12px",
    borderRadius: "6px",
    fontFamily: refStyles.fontFamily,
    fontWeight: "normal",
    fontSize: "14px",
    maxWidth: "240px",
    boxShadow: "0 2px 8px rgba(0,0,0,0.8)",
    opacity: "0",
    visibility: "hidden",
    transition: "opacity 0.25s ease",
    userSelect: "none",
    zIndex: 9998,
  });
  document.body.appendChild(infoPopup);

  function updatePopupContent() {
  let liveShowText = "No live shows currently";

  // doesnt show live show info if dj is "commodore 64"
  if (
    liveDjName &&
    !/wbor'?s commodore\s*64/i.test(liveDjName.trim())
  ) {
    liveShowText = `Live Show: ${liveShowName}${liveDjName ? ` with ${liveDjName}` : ""}`;
  }

  infoPopup.innerHTML = `
    <div><strong>Now Playing:</strong> ${nowPlayingText}</div>
    <div style="margin-top: 8px;"><strong>${liveShowText}</strong></div>
  `;
}


  function positionPopup() {
    const btnRect = infoButton.getBoundingClientRect();
    const popupRect = infoPopup.getBoundingClientRect();
    let left = btnRect.left - popupRect.width - 8;
    let top = btnRect.top + (btnRect.height / 2) - (popupRect.height / 2);

    if (left < 8) {
      left = btnRect.right + 8;
    }
    if (top < 8) top = 8;
    if (top + popupRect.height > window.innerHeight - 8)
      top = window.innerHeight - popupRect.height - 8;

    infoPopup.style.left = `${left}px`;
    infoPopup.style.top = `${top}px`;
  }

  infoButton.addEventListener("mouseenter", () => {
    if (isPopupOpen) return;
    const rect = infoButton.getBoundingClientRect();
    tooltipSpan.style.left = `${rect.left - tooltipSpan.offsetWidth - 8}px`;
    tooltipSpan.style.top = `${rect.top + (rect.height / 2) - 10}px`;
    tooltipSpan.style.opacity = "1";
    tooltipSpan.style.visibility = "visible";
  });

  infoButton.addEventListener("mouseleave", () => {
    tooltipSpan.style.opacity = "0";
    tooltipSpan.style.visibility = "hidden";
  });

  infoButton.addEventListener("click", () => {
    isPopupOpen = !isPopupOpen;
    if (isPopupOpen) {
      updatePopupContent();
      infoPopup.style.opacity = "1";
      infoPopup.style.visibility = "visible";
      tooltipSpan.style.opacity = "0";
      tooltipSpan.style.visibility = "hidden";
      positionPopup();
    } else {
      infoPopup.style.opacity = "0";
      infoPopup.style.visibility = "hidden";
    }
  });

  document.addEventListener("click", (e) => {
    if (!infoPopup.contains(e.target) && e.target !== infoButton) {
      infoPopup.style.opacity = "0";
      infoPopup.style.visibility = "hidden";
      isPopupOpen = false;
    }
  });

  window.addEventListener("resize", () => {
    if (infoPopup.style.visibility === "visible") {
      positionPopup();
    }
  });
})();