Skip to content
← Public packages

@kentcdodds/spotify

Spotify playback, playlist, search, library, and device helpers.

src/__recovered_app_entry.js

869 lines · 32.0 KB · JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// virtual:.__kody_root__/src/app/server.ts
var server_exports = {};
__export(server_exports, {
  default: () => fetch
});

// virtual:.__kody_virtual__/runtime.js
import { AsyncLocalStorage } from "node:async_hooks";
var __kodyRuntimeStorageSymbol = /* @__PURE__ */ Symbol.for("kody.runtimeStorage");
var __globalAny = (
  /** @type {any} */
  globalThis
);
var __kodyRuntimeStorage = __globalAny[__kodyRuntimeStorageSymbol] ?? (__globalAny[__kodyRuntimeStorageSymbol] = new AsyncLocalStorage());
function __kodyReadRuntimeExport(exportName) {
  const currentRuntime = __kodyRuntimeStorage.getStore();
  const runtimeExport = currentRuntime?.[exportName];
  if (runtimeExport == null) {
    throw new Error(
      `kody:runtime export "${exportName}" is not available in this execution context.`
    );
  }
  return runtimeExport;
}
function __kodyRuntimeProxyLabel(exportName) {
  return `[KodyRuntime:${exportName}]`;
}
function __kodyRuntimeProxyInspectionValue(exportName, property) {
  if (property === Symbol.toStringTag) return `KodyRuntime:${exportName}`;
  if (property === "then") return void 0;
  if (property === Symbol.iterator || property === Symbol.asyncIterator) {
    return void 0;
  }
  if (property === Symbol.toPrimitive || property === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || property === "inspect" || property === "toString") {
    return () => __kodyRuntimeProxyLabel(exportName);
  }
  if (property === "valueOf") {
    return () => __kodyCreateRuntimeObjectProxy(exportName);
  }
  return void 0;
}
function __kodyIsRuntimeProxyInspectionProperty(property) {
  return property === Symbol.toStringTag || property === "then" || property === Symbol.iterator || property === Symbol.asyncIterator || property === Symbol.toPrimitive || property === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || property === "inspect" || property === "toString" || property === "valueOf";
}
function __kodyCreateRuntimeObjectProxy(exportName) {
  return new Proxy({}, {
    get(_target, property) {
      const inspectionValue = __kodyRuntimeProxyInspectionValue(
        exportName,
        property
      );
      if (inspectionValue !== void 0 || __kodyIsRuntimeProxyInspectionProperty(property)) {
        return inspectionValue;
      }
      const runtimeExport = __kodyReadRuntimeExport(exportName);
      const value = runtimeExport[property];
      return typeof value === "function" ? value.bind(runtimeExport) : value;
    },
    has(_target, property) {
      if (__kodyIsRuntimeProxyInspectionProperty(property)) return false;
      const currentRuntime = __kodyRuntimeStorage.getStore();
      const runtimeExport = currentRuntime?.[exportName];
      return runtimeExport != null && property in runtimeExport;
    }
  });
}
var __kodyInitialRuntime = __kodyRuntimeStorage.getStore();
var runtime = __kodyInitialRuntime ?? {};
var __kodyKody = __kodyInitialRuntime === void 0 ? __kodyCreateRuntimeObjectProxy("kody") : runtime.kody;
var storage = runtime.storage;
var refreshAccessToken = runtime.refreshAccessToken;
var createAuthenticatedFetch = runtime.createAuthenticatedFetch;
var secretHeaders = runtime.secretHeaders;
var oauthClientCredentials = runtime.oauthClientCredentials;
var packageContext = runtime.packageContext ?? null;
var serviceContext = runtime.serviceContext ?? null;
var service = runtime.service ?? null;
var packageSecrets = runtime.packageSecrets ?? null;
var email = runtime.email ?? null;
var workflows = runtime.workflows ?? null;
var packages = runtime.packages ?? null;
var events = runtime.events ?? null;
var runtime_default = __kodyInitialRuntime === void 0 ? { ...runtime, kody: __kodyKody } : runtime;

// virtual:.__kody_root__/src/lib/spotify-api.ts
var SPOTIFY_BASE_URL = "https://api.spotify.com/v1";
var DEFAULT_FALLBACK_DEVICE_ID = "2084292936";
var DEFAULT_COLLECTION_URI = "spotify:user:kentcdodds:collection";
function buildUrl(path, query) {
  const url = new URL(path.replace(/^\//, ""), SPOTIFY_BASE_URL + "/");
  for (const [key, value] of Object.entries(query ?? {})) {
    if (value === void 0 || value === null || value === "") continue;
    url.searchParams.set(key, String(value));
  }
  return url.toString();
}
function getSpotifyAuthErrorText(error) {
  const values = [
    error?.error,
    error?.error_description,
    error?.message,
    error?.detail,
    error?.detail?.error,
    error?.detail?.error_description,
    error?.detail?.message,
    error?.cause?.error,
    error?.cause?.error_description,
    error?.cause?.message
  ];
  return values.map((value) => typeof value === "string" ? value : "").join(" ").toLowerCase();
}
function isSpotifyInvalidGrantError(error) {
  return getSpotifyAuthErrorText(error).includes("invalid_grant");
}
function normalizeSpotifyAuthError(error) {
  if (!isSpotifyInvalidGrantError(error)) return error;
  const authError = new Error(
    "Spotify authorization expired. Reconnect Spotify at /connect/oauth?provider=spotify, then retry this action."
  );
  authError.name = "SpotifyReauthorizationRequiredError";
  authError.cause = error;
  return authError;
}
async function getSpotifyFetch() {
  try {
    return await createAuthenticatedFetch("spotify");
  } catch (error) {
    throw normalizeSpotifyAuthError(error);
  }
}
async function parseResponseBody(response) {
  const text = await response.text();
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}
function getSpotifyErrorMessage(detail) {
  if (!detail) return null;
  if (typeof detail === "string") return detail;
  if (typeof detail?.error?.message === "string") return detail.error.message;
  if (typeof detail?.error_description === "string") {
    return detail.error_description;
  }
  if (typeof detail?.message === "string") return detail.message;
  if (typeof detail?.error === "string") return detail.error;
  return null;
}
function buildSpotifyError(message, status, detail) {
  const spotifyMessage = getSpotifyErrorMessage(detail);
  const suffix = spotifyMessage ? ": " + spotifyMessage : ".";
  const error = new Error(
    message + " Spotify API responded with status " + status + suffix
  );
  error.name = "SpotifyApiError";
  error.status = status;
  error.detail = detail;
  return error;
}
function assertSpotifyOk(response, data, message, successStatuses = []) {
  if (response.ok || successStatuses.includes(response.status)) return;
  throw buildSpotifyError(message, response.status, data);
}
async function requestSpotify(path, options = {}) {
  const { method = "GET", query, body, headers, allowNoContent = false } = options;
  const spotifyFetch = await getSpotifyFetch();
  let response;
  try {
    response = await spotifyFetch(buildUrl(path, query), {
      method,
      headers: {
        ...body !== void 0 ? { "Content-Type": "application/json" } : {},
        ...headers ?? {}
      },
      ...body !== void 0 ? { body: JSON.stringify(body) } : {}
    });
  } catch (error) {
    throw normalizeSpotifyAuthError(error);
  }
  if (allowNoContent && (response.status === 202 || response.status === 204)) {
    return { response, data: null };
  }
  return {
    response,
    data: await parseResponseBody(response)
  };
}
function normalizeArtistNames(artists) {
  return Array.isArray(artists) ? artists.map((artist) => artist?.name).filter(Boolean) : [];
}
function normalizeDevice(device) {
  if (!device) return null;
  return {
    id: device.id ?? null,
    isActive: Boolean(device.is_active),
    isRestricted: Boolean(device.is_restricted),
    name: device.name ?? "Unknown device",
    supportsVolume: Boolean(device.supports_volume),
    type: device.type ?? "Unknown device",
    volumePercent: Number.isFinite(device.volume_percent) ? device.volume_percent : null
  };
}
function normalizePlaybackItem(item) {
  if (!item) return null;
  return {
    album: item.album?.name ?? null,
    artists: normalizeArtistNames(item.artists),
    durationMs: Number.isFinite(item.duration_ms) ? item.duration_ms : null,
    externalUrl: item.external_urls?.spotify ?? null,
    name: item.name ?? "Unknown track",
    type: item.type ?? "track",
    uri: item.uri ?? null
  };
}
function normalizePlaybackState(playback) {
  if (!playback) return null;
  return {
    contextUri: playback.context?.uri ?? null,
    device: normalizeDevice(playback.device),
    isPlaying: Boolean(playback.is_playing),
    item: normalizePlaybackItem(playback.item),
    progressMs: Number.isFinite(playback.progress_ms) ? playback.progress_ms : null,
    repeatState: playback.repeat_state ?? "off",
    shuffleState: Boolean(playback.shuffle_state)
  };
}
async function getPlaybackState() {
  const { response, data } = await requestSpotify("/me/player", {
    allowNoContent: true
  });
  if (response.status === 204 || response.status === 404) {
    return {
      nothingPlaying: true,
      playback: null,
      status: response.status
    };
  }
  assertSpotifyOk(response, data, "Failed to read Spotify playback state.");
  return {
    nothingPlaying: false,
    playback: data,
    status: response.status
  };
}
async function getAvailableDevices() {
  const { response, data } = await requestSpotify("/me/player/devices");
  assertSpotifyOk(response, data, "Failed to read available Spotify devices.");
  return Array.isArray(data?.devices) ? data.devices : [];
}
async function getQueue(params = {}) {
  // Spotify's queue is session-global; GET /me/player/queue accepts no query params.
  void params;
  const { response, data } = await requestSpotify("/me/player/queue");
  assertSpotifyOk(response, data, "Failed to read the Spotify queue.");
  return data ?? { currently_playing: null, queue: [] };
}
function resolveDeviceId(input = {}) {
  const fallbackDeviceId = input.fallbackDeviceId === void 0 ? DEFAULT_FALLBACK_DEVICE_ID : input.fallbackDeviceId;
  return input.requestedDeviceId ?? input.playback?.device?.id ?? (Array.isArray(input.devices) ? input.devices.find((device) => device?.is_active)?.id ?? input.devices[0]?.id ?? null : null) ?? fallbackDeviceId ?? null;
}
async function requireDeviceId(input = {}) {
  const deviceId = resolveDeviceId(input);
  if (!deviceId) {
    throw new Error("No Spotify device is currently available.");
  }
  return deviceId;
}
async function startOrResumePlayback(params = {}) {
  const playbackState = await getPlaybackState();
  const devices = await getAvailableDevices();
  const deviceId = await requireDeviceId({
    requestedDeviceId: params.deviceId,
    playback: playbackState.playback,
    devices,
    fallbackDeviceId: params.fallbackDeviceId
  });
  const shouldStartCollection = playbackState.nothingPlaying || params.forceCollection === true;
  const body = shouldStartCollection ? {
    context_uri: params.contextUri ?? DEFAULT_COLLECTION_URI,
    offset: { position: 0 }
  } : void 0;
  const { response, data } = await requestSpotify("/me/player/play", {
    method: "PUT",
    query: { device_id: deviceId },
    body,
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to start or resume Spotify playback.", [
    204
  ]);
  return {
    ok: true,
    action: shouldStartCollection ? "started" : "resumed",
    deviceId,
    status: response.status
  };
}
async function pausePlayback(params = {}) {
  const { response, data } = await requestSpotify("/me/player/pause", {
    method: "PUT",
    query: params.deviceId ? { device_id: params.deviceId } : void 0,
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to pause Spotify playback.", [204]);
  return {
    ok: true,
    action: "paused",
    deviceId: params.deviceId ?? null,
    status: response.status
  };
}
async function skipToNext(params = {}) {
  const { response, data } = await requestSpotify("/me/player/next", {
    method: "POST",
    query: params.deviceId ? { device_id: params.deviceId } : void 0,
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to skip to the next Spotify track.", [
    204
  ]);
  return {
    ok: true,
    action: "next",
    status: response.status
  };
}
async function skipToPrevious(params = {}) {
  const { response, data } = await requestSpotify("/me/player/previous", {
    method: "POST",
    query: params.deviceId ? { device_id: params.deviceId } : void 0,
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to skip to the previous Spotify track.", [
    204
  ]);
  return {
    ok: true,
    action: "previous",
    status: response.status
  };
}
async function transferPlayback(params = {}) {
  const deviceId = params.deviceId ?? params.device_id ?? null;
  if (!deviceId) {
    throw new Error("A deviceId is required to transfer Spotify playback.");
  }
  const { response, data } = await requestSpotify("/me/player", {
    method: "PUT",
    body: {
      device_ids: [deviceId],
      play: params.play !== false
    },
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to transfer Spotify playback.", [204]);
  return {
    ok: true,
    action: "transferred",
    deviceId,
    play: params.play !== false,
    status: response.status
  };
}
async function addToQueue(params = {}) {
  const uri = params.uri ?? params.spotifyUri ?? null;
  if (!uri) {
    throw new Error("A Spotify track or episode URI is required.");
  }
  const { response, data } = await requestSpotify("/me/player/queue", {
    method: "POST",
    query: {
      uri,
      device_id: params.deviceId ?? params.device_id
    },
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to add the item to the Spotify queue.", [
    204
  ]);
  return {
    ok: true,
    action: "queued",
    uri,
    status: response.status
  };
}
async function setVolume(params = {}) {
  const volumePercent = Math.round(
    Number(params.volumePercent ?? params.volume ?? NaN)
  );
  if (!Number.isFinite(volumePercent) || volumePercent < 0 || volumePercent > 100) {
    throw new Error("Volume must be a number between 0 and 100.");
  }
  const playbackState = await getPlaybackState();
  const devices = await getAvailableDevices();
  const deviceId = await requireDeviceId({
    requestedDeviceId: params.deviceId ?? params.device_id,
    playback: playbackState.playback,
    devices,
    fallbackDeviceId: null
  });
  const { response, data } = await requestSpotify("/me/player/volume", {
    method: "PUT",
    query: {
      device_id: deviceId,
      volume_percent: volumePercent
    },
    allowNoContent: true
  });
  assertSpotifyOk(response, data, "Failed to set Spotify volume.", [204]);
  return {
    ok: true,
    action: "volume-set",
    deviceId,
    volumePercent,
    status: response.status
  };
}

// virtual:.__kody_root__/src/exports/add-to-queue.ts
async function spotifyAddToQueue(params = {}) {
  return await addToQueue({
    uri: params.uri ?? params.spotifyUri,
    deviceId: params.deviceId ?? params.device_id
  });
}

// virtual:.__kody_root__/src/exports/get-queue.ts
async function getSpotifyQueue(params = {}) {
  const queue = await getQueue({
    deviceId: params.deviceId ?? params.device_id
  });
  return {
    currentlyPlaying: normalizePlaybackItem(queue.currently_playing),
    queue: Array.isArray(queue.queue) ? queue.queue.map(normalizePlaybackItem) : []
  };
}

// virtual:.__kody_root__/src/exports/play-pause.ts
async function playPause(params = {}) {
  const current = await getPlaybackState();
  if (current.nothingPlaying) {
    return await startOrResumePlayback({
      deviceId: params.deviceId,
      fallbackDeviceId: params.fallbackDeviceId
    });
  }
  if (current.playback?.is_playing) {
    return await pausePlayback({
      deviceId: params.deviceId ?? current.playback?.device?.id ?? void 0
    });
  }
  return await startOrResumePlayback({
    deviceId: params.deviceId ?? current.playback?.device?.id ?? void 0,
    fallbackDeviceId: params.fallbackDeviceId,
    forceCollection: false
  });
}

// virtual:.__kody_root__/src/exports/set-volume.ts
async function spotifySetVolume(params = {}) {
  return await setVolume({
    deviceId: params.deviceId ?? params.device_id,
    volumePercent: params.volumePercent ?? params.volume
  });
}

// virtual:.__kody_root__/src/exports/skip-next.ts
async function skipNext(params = {}) {
  return await skipToNext({
    deviceId: params.deviceId ?? params.device_id
  });
}

// virtual:.__kody_root__/src/exports/skip-previous.ts
async function skipPrevious(params = {}) {
  return await skipToPrevious({
    deviceId: params.deviceId ?? params.device_id
  });
}

// virtual:.__kody_root__/src/exports/transfer-playback.ts
async function spotifyTransferPlayback(params = {}) {
  return await transferPlayback({
    deviceId: params.deviceId ?? params.device_id,
    play: params.play
  });
}

// virtual:.__kody_root__/src/lib/remote-state.ts
async function buildRemoteState(options = {}) {
  const devices = await getAvailableDevices();
  const playbackState = await getPlaybackState();
  const normalizedDevices = devices.map(normalizeDevice).filter(Boolean);
  const playback = normalizePlaybackState(playbackState.playback);
  let message = options.message ?? "Ready.";
  if (!normalizedDevices.length) {
    message = options.message ?? "No Spotify devices are currently available.";
  } else if (playbackState.nothingPlaying) {
    message = options.message ?? "Spotify is connected, but nothing is currently playing right now.";
  }
  return {
    ok: true,
    message,
    hasActiveDevice: normalizedDevices.length > 0,
    devices: normalizedDevices,
    playback,
    nothingPlaying: playbackState.nothingPlaying
  };
}

// virtual:.__kody_root__/src/exports/playback-controller.ts
async function playbackController(params = {}) {
  const action = params.action ?? "status";
  switch (action) {
    case "status":
      return await buildRemoteState();
    case "state":
      return await buildRemoteState();
    case "queue": {
      const [state, queue] = await Promise.all([
        buildRemoteState(),
        getSpotifyQueue(params)
      ]);
      return {
        ...state,
        queue
      };
    }
    case "play":
      await startOrResumePlayback({
        deviceId: params.deviceId,
        fallbackDeviceId: params.fallbackDeviceId
      });
      return await buildRemoteState({ message: "Playback resumed." });
    case "pause":
      await pausePlayback({ deviceId: params.deviceId });
      return await buildRemoteState({ message: "Playback paused." });
    case "play-pause": {
      const result = await playPause(params);
      const message = result.action === "paused" ? "Playback paused." : result.action === "resumed" ? "Playback resumed." : "Playback started on the fallback device.";
      return await buildRemoteState({ message });
    }
    case "next":
      await skipNext(params);
      return await buildRemoteState({ message: "Skipped to the next track." });
    case "previous":
      await skipPrevious(params);
      return await buildRemoteState({ message: "Returned to the previous track." });
    case "transfer":
      await spotifyTransferPlayback(params);
      return await buildRemoteState({
        message: "Playback transferred to the selected device."
      });
    case "set-volume":
      await spotifySetVolume(params);
      return await buildRemoteState({ message: "Volume updated." });
    case "add-to-queue": {
      await spotifyAddToQueue(params);
      const [state, queue] = await Promise.all([
        buildRemoteState({ message: "Added the item to the queue." }),
        getSpotifyQueue(params)
      ]);
      return {
        ...state,
        queue
      };
    }
    default:
      throw new Error("Unknown Spotify controller action: " + String(action));
  }
}

// virtual:.__kody_root__/src/app/server.ts
function json(data, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      "cache-control": "no-store",
      "content-type": "application/json; charset=utf-8"
    }
  });
}
function html(content) {
  return new Response(content, {
    headers: {
      "cache-control": "no-store",
      "content-type": "text/html; charset=utf-8"
    }
  });
}
function escapeHtml(value) {
  return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
}
function getBaseHref(requestUrl) {
  const url = new URL(requestUrl);
  const normalizedPath = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
  return escapeHtml(normalizedPath);
}
function renderPage(requestUrl) {
  const baseHref = getBaseHref(requestUrl);
  return [
    "<!doctype html>",
    '<html lang="en">',
    "<head>",
    '  <meta charset="utf-8" />',
    '  <meta name="viewport" content="width=device-width, initial-scale=1" />',
    '  <base href="' + baseHref + '" />',
    "  <title>Spotify Playback Remote</title>",
    "  <style>",
    "    :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }",
    "    body { margin: 0; background: #0f1115; color: #f5f7fb; }",
    "    .shell { max-width: 560px; margin: 0 auto; padding: 24px; }",
    "    .panel { border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 20px; background: #171b22; }",
    "    h1 { margin: 0 0 8px; font-size: 1.9rem; }",
    "    .subtle, .meta { color: #aab4c3; }",
    "    .message { margin: 16px 0; padding: 12px 14px; border-radius: 12px; background: rgba(30, 215, 96, 0.12); border: 1px solid rgba(30, 215, 96, 0.22); }",
    "    .message.error { background: rgba(255, 90, 90, 0.12); border-color: rgba(255, 90, 90, 0.22); }",
    "    .card { margin: 16px 0; padding: 16px; border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; background: rgba(255,255,255,0.03); }",
    "    .label { margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.75rem; color: #7ee0a8; }",
    "    .row { display: flex; gap: 12px; flex-wrap: wrap; }",
    "    select, button { border-radius: 10px; font: inherit; }",
    "    select { flex: 1 1 260px; min-width: 0; padding: 12px; background: #0f1115; color: #f5f7fb; border: 1px solid rgba(255,255,255,0.12); }",
    "    button { cursor: pointer; border: none; padding: 12px 16px; font-weight: 700; background: #1ed760; color: #08130b; }",
    "    button.secondary { background: rgba(255,255,255,0.12); color: #f5f7fb; }",
    "    button.ghost { background: transparent; color: #f5f7fb; border: 1px solid rgba(255,255,255,0.12); }",
    "    button:disabled, select:disabled { opacity: 0.6; cursor: wait; }",
    "    a { color: #8ef0b5; }",
    "  </style>",
    "</head>",
    "<body>",
    '  <main class="shell">',
    '    <section class="panel">',
    '      <p class="label">Spotify</p>',
    "      <h1>Playback Remote</h1>",
    '      <p class="subtle">Package-first controls for the current Spotify session.</p>',
    '      <div id="message" class="message">Loading playback status...</div>',
    '      <section class="card">',
    '        <p class="label">Now Playing</p>',
    '        <h2 id="track-title">Loading...</h2>',
    '        <p id="track-artist" class="subtle"></p>',
    '        <p id="track-album" class="meta"></p>',
    '        <p id="track-meta" class="meta"></p>',
    '        <a id="track-link" hidden target="_blank" rel="noreferrer">Open in Spotify</a>',
    "      </section>",
    '      <section class="card">',
    '        <p class="label">Device</p>',
    '        <div class="row">',
    '          <select id="device-select"></select>',
    '          <button id="transfer-button" class="secondary">Use Device</button>',
    "        </div>",
    "      </section>",
    '      <div class="row">',
    '        <button data-action="previous" class="secondary">Previous</button>',
    '        <button data-action="play-pause">Play / Pause</button>',
    '        <button data-action="next" class="secondary">Next</button>',
    '        <button id="refresh-button" class="ghost">Refresh</button>',
    "      </div>",
    "    </section>",
    "  </main>",
    '  <script type="module">',
    '    const messageEl = document.getElementById("message")',
    '    const trackTitleEl = document.getElementById("track-title")',
    '    const trackArtistEl = document.getElementById("track-artist")',
    '    const trackAlbumEl = document.getElementById("track-album")',
    '    const trackMetaEl = document.getElementById("track-meta")',
    '    const trackLinkEl = document.getElementById("track-link")',
    '    const deviceSelectEl = document.getElementById("device-select")',
    '    const transferButtonEl = document.getElementById("transfer-button")',
    '    const refreshButtonEl = document.getElementById("refresh-button")',
    '    const actionButtons = Array.from(document.querySelectorAll("[data-action]"))',
    "    let busy = false",
    '    let selectedDeviceId = ""',
    "    function setBusy(nextBusy) {",
    "      busy = nextBusy",
    "      deviceSelectEl.disabled = nextBusy",
    "      transferButtonEl.disabled = nextBusy",
    "      refreshButtonEl.disabled = nextBusy",
    "      actionButtons.forEach((button) => { button.disabled = nextBusy })",
    "    }",
    "    function setMessage(text, isError = false) {",
    '      messageEl.textContent = text || ""',
    '      messageEl.classList.toggle("error", Boolean(isError))',
    "    }",
    "    function formatDuration(milliseconds) {",
    '      if (!Number.isFinite(milliseconds) || milliseconds < 0) return ""',
    "      const totalSeconds = Math.floor(milliseconds / 1000)",
    "      const minutes = Math.floor(totalSeconds / 60)",
    "      const seconds = totalSeconds % 60",
    '      return String(minutes) + ":" + String(seconds).padStart(2, "0")',
    "    }",
    "    function getSelectedDeviceId() {",
    '      return typeof deviceSelectEl.value === "string" ? deviceSelectEl.value : ""',
    "    }",
    "    function renderDevices(payload) {",
    "      const devices = Array.isArray(payload?.devices) ? payload.devices : []",
    '      deviceSelectEl.innerHTML = ""',
    "      if (!devices.length) {",
    '        const option = document.createElement("option")',
    '        option.value = ""',
    '        option.textContent = "No Spotify devices available"',
    "        deviceSelectEl.append(option)",
    "        return",
    "      }",
    "      if (!selectedDeviceId || !devices.some((device) => device.id === selectedDeviceId)) {",
    '        selectedDeviceId = devices.find((device) => device.isActive)?.id || devices[0]?.id || ""',
    "      }",
    "      devices.forEach((device) => {",
    '        const option = document.createElement("option")',
    '        option.value = device.id || ""',
    '        const labels = [device.name || "Unknown device"]',
    '        if (device.type) labels.push("(" + device.type + ")")',
    '        if (device.isActive) labels.push("active")',
    '        option.textContent = labels.join(" ")',
    "        option.selected = option.value === selectedDeviceId",
    "        deviceSelectEl.append(option)",
    "      })",
    "    }",
    "    function renderPlayback(payload) {",
    "      const playback = payload?.playback || null",
    "      const item = playback?.item || null",
    "      if (!item) {",
    '        trackTitleEl.textContent = "Nothing currently playing"',
    '        trackArtistEl.textContent = payload?.hasActiveDevice ? "Select a device and press Play / Pause to start Spotify." : "Open Spotify on a device, then refresh."',
    '        trackAlbumEl.textContent = ""',
    '        trackMetaEl.textContent = payload?.hasActiveDevice ? "Spotify is connected, but there is no active track right now." : "No active Spotify device is available."',
    "        trackLinkEl.hidden = true",
    '        trackLinkEl.removeAttribute("href")',
    "        return",
    "      }",
    '      trackTitleEl.textContent = item.name || "Unknown track"',
    '      trackArtistEl.textContent = Array.isArray(item.artists) && item.artists.length ? item.artists.join(", ") : "Unknown artist"',
    '      trackAlbumEl.textContent = item.album ? "Album: " + item.album : ""',
    "      const meta = []",
    '      meta.push(playback?.isPlaying ? "Playing" : "Paused")',
    '      if (playback?.device?.name) meta.push("on " + playback.device.name)',
    "      if (Number.isFinite(playback?.progressMs) && Number.isFinite(item.durationMs)) {",
    '        meta.push(formatDuration(playback.progressMs) + " / " + formatDuration(item.durationMs))',
    "      }",
    '      trackMetaEl.textContent = meta.join(" \u2022 ")',
    "      if (item.externalUrl) {",
    "        trackLinkEl.hidden = false",
    "        trackLinkEl.href = item.externalUrl",
    "      } else {",
    "        trackLinkEl.hidden = true",
    '        trackLinkEl.removeAttribute("href")',
    "      }",
    "    }",
    "    function render(payload) {",
    "      renderDevices(payload)",
    "      renderPlayback(payload)",
    '      setMessage(payload?.message || "Ready.")',
    "    }",
    "    async function request(path, init) {",
    "      const response = await fetch(path, init)",
    "      const text = await response.text()",
    "      const payload = text ? JSON.parse(text) : null",
    "      if (!response.ok) {",
    '        throw new Error(payload?.error || payload?.message || "Request failed.")',
    "      }",
    "      return payload",
    "    }",
    "    async function refreshState() {",
    "      setBusy(true)",
    "      try {",
    '        render(await request("api/state"))',
    "      } catch (error) {",
    "        setMessage(error instanceof Error ? error.message : String(error), true)",
    "      } finally {",
    "        setBusy(false)",
    "      }",
    "    }",
    "    async function runAction(action) {",
    "      setBusy(true)",
    "      try {",
    '        const payload = await request("api/action", {',
    '          method: "POST",',
    '          headers: { "content-type": "application/json" },',
    "          body: JSON.stringify({",
    "            action,",
    "            deviceId: getSelectedDeviceId() || undefined,",
    "          }),",
    "        })",
    "        render(payload)",
    "      } catch (error) {",
    "        setMessage(error instanceof Error ? error.message : String(error), true)",
    "      } finally {",
    "        setBusy(false)",
    "      }",
    "    }",
    '    deviceSelectEl.addEventListener("change", () => {',
    "      selectedDeviceId = getSelectedDeviceId()",
    "    })",
    '    transferButtonEl.addEventListener("click", () => {',
    '      void runAction("transfer")',
    "    })",
    '    refreshButtonEl.addEventListener("click", () => {',
    "      void refreshState()",
    "    })",
    "    actionButtons.forEach((button) => {",
    '      button.addEventListener("click", () => {',
    '        const action = button.getAttribute("data-action")',
    "        if (action) void runAction(action)",
    "      })",
    "    })",
    "    void refreshState()",
    "    window.setInterval(() => {",
    "      if (!busy) void refreshState()",
    "    }, 15000)",
    "  <\/script>",
    "</body>",
    "</html>"
  ].join("\n");
}
async function fetch(request) {
  const url = new URL(request.url);
  try {
    if (url.pathname.endsWith("/api/state") && request.method === "GET") {
      return json(await playbackController({ action: "status" }));
    }
    if (url.pathname.endsWith("/api/action") && request.method === "POST") {
      const payload = await request.json().catch(() => ({}));
      return json(await playbackController(payload ?? {}));
    }
    return html(renderPage(request.url));
  } catch (error) {
    return json(
      {
        ok: false,
        error: error instanceof Error ? error.message : String(error)
      },
      500
    );
  }
}

// virtual:.__kody_root__/.__kody_app_entry__.js
function resolvePackageAppHandler() {
  const candidate = fetch ?? server_exports;
  if (typeof candidate === "function") {
    return candidate;
  }
  if (candidate && typeof candidate.fetch === "function") {
    return candidate.fetch.bind(candidate);
  }
  if (typeof void 0 === "function") {
    return void 0;
  }
  throw new Error(
    "Kody package apps must export a fetch handler via default export or named fetch."
  );
}
var handler = resolvePackageAppHandler();
var kody_app_entry_default = {
  async fetch(request, env, ctx) {
    return await handler(request, env, ctx);
  }
};
export {
  kody_app_entry_default as default
};