← Public packages
@kentcdodds/spotify
Spotify playback, playlist, search, library, and device helpers.
src/__recovered_app_entry.js
869 lines · 32.0 KB · JavaScriptvar __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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
}
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
};