Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/spotify

src/lib/openapi-client.ts

352 lines · 10.6 KB · TypeScript
/**
 * Scaffolded Spotify OpenAPI client (dependency-free ESM).
 *
 * Source: openapi_client_scaffold against
 * https://raw.githubusercontent.com/sonallux/spotify-web-api/main/fixed-spotify-open-api.yml
 * Auth: createAuthenticatedFetch(provider); default provider "spotify".
 */

import { createAuthenticatedFetch } from 'kody:runtime'

export type QueryValue = string | number | boolean | null | undefined
export type QueryInput = Record<string, QueryValue | QueryValue[]>

export type ScaffoldInput = {
	params?: Record<string, unknown>
	query?: QueryInput
	headers?: Record<string, string>
	body?: unknown
}

export type ScaffoldOptions = {
	/** Saved OAuth integration name. Defaults to "spotify". */
	provider?: string
	/** Bypass integration auth (tests). */
	fetchImpl?: typeof fetch
}

const API_BASE_URL = 'https://api.spotify.com/v1'

export function buildUrl(pathTemplate: string, params: Record<string, unknown> = {}): string {
	return API_BASE_URL + pathTemplate.replace(/\{([^}]+)\}/g, (_match, name: string) => {
		const value = params[name]
		if (value === undefined || value === null) {
			throw new Error(`Missing required path parameter: ${name}`)
		}
		return encodeURIComponent(String(value))
	})
}

export function appendQuery(url: string, query: QueryInput = {}): string {
	const search = new URLSearchParams()
	for (const [key, value] of Object.entries(query)) {
		if (value === undefined || value === null || value === '') continue
		if (Array.isArray(value)) {
			for (const item of value) {
				if (item === undefined || item === null || item === '') continue
				search.append(key, String(item))
			}
			continue
		}
		search.append(key, String(value))
	}
	const qs = search.toString()
	return qs ? `${url}?${qs}` : url
}

function mergeHeaders(
	userHeaders: Record<string, string> | undefined,
	authHeaders: Record<string, string>,
): Record<string, string> {
	const merged: Record<string, string> = { ...(userHeaders ?? {}) }
	for (const [key, value] of Object.entries(authHeaders)) {
		const lower = key.toLowerCase()
		for (const existing of Object.keys(merged)) {
			if (existing.toLowerCase() === lower) delete merged[existing]
		}
		merged[key] = value
	}
	return merged
}

function hasHeader(headers: Record<string, string>, name: string): boolean {
	const lower = name.toLowerCase()
	return Object.keys(headers).some((key) => key.toLowerCase() === lower)
}

const authedFetchByProvider = new Map<string, typeof fetch>()

async function resolveFetch(options: ScaffoldOptions = {}): Promise<typeof fetch> {
	if (options.fetchImpl) return options.fetchImpl
	const provider = options.provider ?? 'spotify'
	let cached = authedFetchByProvider.get(provider)
	if (!cached) {
		cached = await createAuthenticatedFetch(provider)
		authedFetchByProvider.set(provider, cached)
	}
	return cached
}

async function call(
	method: string,
	pathTemplate: string,
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
	requiredParams: string[] = [],
): Promise<Response> {
	const params = input.params ?? {}
	for (const name of requiredParams) {
		if (params[name] === undefined || params[name] === null) {
			throw new Error(`Missing required path parameter: ${name}`)
		}
	}
	const url = appendQuery(buildUrl(pathTemplate, params), input.query)
	const headers = mergeHeaders(input.headers, {})
	let body: string | undefined
	if (input.body !== undefined) {
		body = JSON.stringify(input.body)
		if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json'
	}
	const fetchImpl = await resolveFetch(options)
	return fetchImpl(url, { method, headers, body })
}

/** GET /me — Get Current User's Profile */
export async function getCurrentUsersProfile(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me', input, options)
}

/** GET /me/tracks — Get User's Saved Tracks */
export async function getUsersSavedTracks(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/tracks', input, options)
}

/** GET /me/player — Get Playback State */
export async function getInformationAboutTheUsersCurrentPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/player', input, options)
}

/** GET /me/player/devices — Get Available Devices */
export async function getAUsersAvailableDevices(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/player/devices', input, options)
}

/** PUT /me/player/play — Start/Resume Playback */
export async function startAUsersPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player/play', input, options)
}

/** PUT /me/player/pause — Pause Playback */
export async function pauseAUsersPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player/pause', input, options)
}

/** POST /me/player/next — Skip To Next */
export async function skipUsersPlaybackToNextTrack(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('POST', '/me/player/next', input, options)
}

/** POST /me/player/previous — Skip To Previous */
export async function skipUsersPlaybackToPreviousTrack(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('POST', '/me/player/previous', input, options)
}

/** PUT /me/player — Transfer Playback */
export async function transferAUsersPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player', input, options)
}

/** PUT /me/player/volume — Set Playback Volume */
export async function setVolumeForUsersPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player/volume', input, options)
}

/** PUT /me/player/seek — Seek To Position */
export async function seekToPositionInCurrentlyPlayingTrack(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player/seek', input, options)
}

/** PUT /me/player/repeat — Set Repeat Mode */
export async function setRepeatModeOnUsersPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player/repeat', input, options)
}

/** PUT /me/player/shuffle — Toggle Playback Shuffle */
export async function toggleShuffleForUsersPlayback(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/player/shuffle', input, options)
}

/** POST /me/player/queue — Add Item to Playback Queue */
export async function addToQueue(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('POST', '/me/player/queue', input, options)
}

/** GET /me/player/queue — Get the User's Queue */
export async function getQueue(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/player/queue', input, options)
}

/** POST /me/playlists — Create Playlist */
export async function createPlaylist(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('POST', '/me/playlists', input, options)
}

/** POST /playlists/{playlist_id}/items — Add Items to Playlist */
export async function addItemsToPlaylist(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('POST', '/playlists/{playlist_id}/items', input, options, ['playlist_id'])
}

/** DELETE /playlists/{playlist_id}/items — Remove Playlist Items */
export async function removeItemsPlaylist(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('DELETE', '/playlists/{playlist_id}/items', input, options, ['playlist_id'])
}

/** PUT /me/library — Save Items to Library */
export async function saveLibraryItems(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/me/library', input, options)
}

/** PUT /playlists/{playlist_id}/followers — Follow Playlist */
export async function followPlaylist(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('PUT', '/playlists/{playlist_id}/followers', input, options, ['playlist_id'])
}

/** GET /playlists/{playlist_id} — Get Playlist */
export async function getPlaylist(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/playlists/{playlist_id}', input, options, ['playlist_id'])
}

/** GET /playlists/{playlist_id}/items — Get Playlist Items */
export async function getPlaylistsItems(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/playlists/{playlist_id}/items', input, options, ['playlist_id'])
}

/** GET /me/playlists — Get Current User's Playlists */
export async function getAListOfCurrentUsersPlaylists(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/playlists', input, options)
}

/** GET /me/player/recently-played — Get Recently Played Tracks */
export async function getRecentlyPlayed(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/player/recently-played', input, options)
}

/** GET /recommendations — Get Recommendations */
export async function getRecommendations(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/recommendations', input, options)
}

/** GET /me/top/artists — Get User's Top Artists */
export async function getUsersTopArtists(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/top/artists', input, options)
}

/** GET /me/top/tracks — Get User's Top Tracks */
export async function getUsersTopTracks(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/me/top/tracks', input, options)
}

/** GET /tracks/{id} — Get Track */
export async function getTrack(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/tracks/{id}', input, options, ['id'])
}

/** GET /tracks — Get Several Tracks */
export async function getSeveralTracks(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/tracks', input, options)
}

/** GET /search — Search for Item */
export async function search(
	input: ScaffoldInput = {},
	options: ScaffoldOptions = {},
): Promise<Response> {
	return call('GET', '/search', input, options)
}