From 55d4d406a268f40aeb8f0ddc7c912cf23fbfcf20 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sun, 15 Feb 2026 20:48:14 +0100 Subject: [PATCH 1/4] changes for spotify since feb 2026: - limit parameter has changed from 50 to 10 (otherwise we get API error now) - search for music titles has been improved to contain also special characters. Note: - a spotify premium account is necessary to use the spoifty via API - therefore it is good, that we have localPlayer as alternative to spoify player --- ts/packages/agents/player/src/client.ts | 4 ++-- ts/packages/agents/player/src/endpoints.ts | 18 ++++++++++++++---- ts/packages/agents/player/src/search.ts | 12 ++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ts/packages/agents/player/src/client.ts b/ts/packages/agents/player/src/client.ts index 91d6aef55a..4aca584002 100644 --- a/ts/packages/agents/player/src/client.ts +++ b/ts/packages/agents/player/src/client.ts @@ -514,7 +514,7 @@ export async function searchTracks( const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 50, + limit: 10, offset: 0, }; const data = await search(query, context.service); @@ -530,7 +530,7 @@ export async function searchForPlaylists( const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "playlist", - limit: 20, + limit: 10, offset: 0, }; const data = await search(query, context.service); diff --git a/ts/packages/agents/player/src/endpoints.ts b/ts/packages/agents/player/src/endpoints.ts index fb4d91ecd9..843c9eeb0f 100644 --- a/ts/packages/agents/player/src/endpoints.ts +++ b/ts/packages/agents/player/src/endpoints.ts @@ -373,6 +373,7 @@ export async function getArtistTopTracks(service: SpotifyService, id: string) { return fetchGet( service, `https://api.spotify.com/v1/artists/${encodeURIComponent(id)}/top-tracks`, + { market: "US" }, ); } @@ -707,8 +708,17 @@ export async function setVolume( } function getUrlWithParams(urlString: string, queryParams: Record) { - const params = new URLSearchParams(queryParams); - const url = new URL(urlString); - url.search = params.toString(); - return url.toString(); + // Use encodeURIComponent for standard URL percent-encoding instead of + // URLSearchParams which uses application/x-www-form-urlencoded (encodes + // spaces as '+' and over-encodes characters like ()!). Spotify's API + // expects standard percent-encoding. + const parts: string[] = []; + for (const [key, value] of Object.entries(queryParams)) { + if (value !== undefined && value !== null) { + parts.push( + `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`, + ); + } + } + return parts.length > 0 ? `${urlString}?${parts.join("&")}` : urlString; } diff --git a/ts/packages/agents/player/src/search.ts b/ts/packages/agents/player/src/search.ts index 7645cc80ee..cff71f35cd 100644 --- a/ts/packages/agents/player/src/search.ts +++ b/ts/packages/agents/player/src/search.ts @@ -56,7 +56,7 @@ export async function searchArtists( const query: SpotifyApi.SearchForItemParameterObject = { q: `artist:${quoteString(searchTerm)}`, type: "artist", - limit: 50, + limit: 10, offset: 0, }; return search(query, context.service); @@ -77,7 +77,7 @@ export async function searchAlbums( const searchQuery: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "album", - limit: 50, + limit: 10, offset: 0, }; const result = await search(searchQuery, context.service); @@ -386,7 +386,7 @@ export async function findArtistTracksWithGenre( const param: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 50, + limit: 10, offset: 0, }; const result = await search(param, context.service); @@ -489,7 +489,7 @@ async function expandMovementTracks( const param: SpotifyApi.SearchForItemParameterObject = { q: toQueryString({ ...originalQuery, album: [album] }), type: "track", - limit: 50, + limit: 10, offset: 0, }; const result = await search(param, context.service); @@ -564,7 +564,7 @@ export async function findTracksWithGenre( const param: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 50, + limit: 10, offset: 0, }; const result = await search(param, context.service); @@ -647,7 +647,7 @@ export async function findTracks( const param: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 50, + limit: 10, offset: 0, }; const result = await search(param, context.service); From a7e7570a58dc9928ca8a56dbdf3dc91644cf5f78 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sun, 15 Feb 2026 20:55:25 +0100 Subject: [PATCH 2/4] Update ts/packages/agents/player/src/endpoints.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts/packages/agents/player/src/endpoints.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ts/packages/agents/player/src/endpoints.ts b/ts/packages/agents/player/src/endpoints.ts index 843c9eeb0f..2c83dfc13d 100644 --- a/ts/packages/agents/player/src/endpoints.ts +++ b/ts/packages/agents/player/src/endpoints.ts @@ -709,9 +709,9 @@ export async function setVolume( function getUrlWithParams(urlString: string, queryParams: Record) { // Use encodeURIComponent for standard URL percent-encoding instead of - // URLSearchParams which uses application/x-www-form-urlencoded (encodes - // spaces as '+' and over-encodes characters like ()!). Spotify's API - // expects standard percent-encoding. + // URLSearchParams, which uses application/x-www-form-urlencoded semantics + // (encoding spaces as '+' rather than '%20'). Spotify's API expects spaces + // to be percent-encoded as '%20'. const parts: string[] = []; for (const [key, value] of Object.entries(queryParams)) { if (value !== undefined && value !== null) { From c3d591910b7db69363ec2aef6779757e02014dda Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Tue, 17 Feb 2026 08:36:22 +0100 Subject: [PATCH 3/4] documented new spotify max API limits and made them obvious and not magic numbers --- ts/packages/agents/player/src/client.ts | 6 ++++-- ts/packages/agents/player/src/endpoints.ts | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ts/packages/agents/player/src/client.ts b/ts/packages/agents/player/src/client.ts index 4aca584002..77c73c76d6 100644 --- a/ts/packages/agents/player/src/client.ts +++ b/ts/packages/agents/player/src/client.ts @@ -511,10 +511,11 @@ export async function searchTracks( queryString: string, context: IClientContext, ) { + const MAX_TRACK_LIMIT = 50; const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 10, + limit: MAX_TRACK_LIMIT, offset: 0, }; const data = await search(query, context.service); @@ -527,10 +528,11 @@ export async function searchForPlaylists( queryString: string, context: IClientContext, ) { + const MAX_PLAYLIST_LIMIT = 50; const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "playlist", - limit: 10, + limit: MAX_PLAYLIST_LIMIT, offset: 0, }; const data = await search(query, context.service); diff --git a/ts/packages/agents/player/src/endpoints.ts b/ts/packages/agents/player/src/endpoints.ts index 2c83dfc13d..b862c40a7a 100644 --- a/ts/packages/agents/player/src/endpoints.ts +++ b/ts/packages/agents/player/src/endpoints.ts @@ -373,7 +373,6 @@ export async function getArtistTopTracks(service: SpotifyService, id: string) { return fetchGet( service, `https://api.spotify.com/v1/artists/${encodeURIComponent(id)}/top-tracks`, - { market: "US" }, ); } From 3196c1b14c0fbfda8fa4fbd68a0873a47d1a9374 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Tue, 17 Feb 2026 23:17:27 +0100 Subject: [PATCH 4/4] Refactor track and playlist search limits to use constants for better maintainability limitMax is 50 for API calls searchResultLimits is 10 fetches fewer results since these are targeted searches where only the top matches matter --- ts/packages/agents/player/src/client.ts | 7 +++---- ts/packages/agents/player/src/endpoints.ts | 5 +++-- ts/packages/agents/player/src/search.ts | 15 +++++++++------ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/ts/packages/agents/player/src/client.ts b/ts/packages/agents/player/src/client.ts index ffe73a1fd6..e61ff7559c 100644 --- a/ts/packages/agents/player/src/client.ts +++ b/ts/packages/agents/player/src/client.ts @@ -52,6 +52,7 @@ import { addTracksToPlaylist, getRecommendationsFromTrackCollection, getRecentlyPlayed, + limitMax, } from "./endpoints.js"; import { htmlStatus, printStatus } from "./playback.js"; import { SpotifyService } from "./service.js"; @@ -448,11 +449,10 @@ export async function searchTracks( queryString: string, context: IClientContext, ) { - const MAX_TRACK_LIMIT = 50; const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: MAX_TRACK_LIMIT, + limit: limitMax, offset: 0, }; const data = await search(query, context.service); @@ -465,11 +465,10 @@ export async function searchForPlaylists( queryString: string, context: IClientContext, ) { - const MAX_PLAYLIST_LIMIT = 50; const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "playlist", - limit: MAX_PLAYLIST_LIMIT, + limit: limitMax, offset: 0, }; const data = await search(query, context.service); diff --git a/ts/packages/agents/player/src/endpoints.ts b/ts/packages/agents/player/src/endpoints.ts index b862c40a7a..85e9d18791 100644 --- a/ts/packages/agents/player/src/endpoints.ts +++ b/ts/packages/agents/player/src/endpoints.ts @@ -8,7 +8,8 @@ import { createFetchError } from "./utils.js"; const debugSpotifyRest = registerDebug("typeagent:spotify:rest"); const debugSpotifyRestVerbose = registerDebug("typeagent:spotify-verbose:rest"); -const limitMax = 50; +/** Maximum number of items per Spotify API request */ +export const limitMax = 50; export async function search( query: SpotifyApi.SearchForItemParameterObject, @@ -483,7 +484,7 @@ export async function getQueue(service: SpotifyService) { return fetchGet( service, "https://api.spotify.com/v1/me/player/queue", - { limit: 50 }, + { limit: limitMax }, ); } diff --git a/ts/packages/agents/player/src/search.ts b/ts/packages/agents/player/src/search.ts index cff71f35cd..58119741b8 100644 --- a/ts/packages/agents/player/src/search.ts +++ b/ts/packages/agents/player/src/search.ts @@ -21,6 +21,9 @@ const debugReuse = registerDebug("typeagent:spotify:search:reuse"); const debugVerbose = registerDebug("typeagent:spotify-verbose:search"); const debugError = registerDebug("typeagent:spotify:search:error"); +/** Number of results to fetch for targeted searches */ +const searchResultLimit = 10; + export type SpotifyQuery = { track?: string[] | undefined; album?: string[] | undefined; @@ -56,7 +59,7 @@ export async function searchArtists( const query: SpotifyApi.SearchForItemParameterObject = { q: `artist:${quoteString(searchTerm)}`, type: "artist", - limit: 10, + limit: searchResultLimit, offset: 0, }; return search(query, context.service); @@ -77,7 +80,7 @@ export async function searchAlbums( const searchQuery: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "album", - limit: 10, + limit: searchResultLimit, offset: 0, }; const result = await search(searchQuery, context.service); @@ -386,7 +389,7 @@ export async function findArtistTracksWithGenre( const param: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 10, + limit: searchResultLimit, offset: 0, }; const result = await search(param, context.service); @@ -489,7 +492,7 @@ async function expandMovementTracks( const param: SpotifyApi.SearchForItemParameterObject = { q: toQueryString({ ...originalQuery, album: [album] }), type: "track", - limit: 10, + limit: searchResultLimit, offset: 0, }; const result = await search(param, context.service); @@ -564,7 +567,7 @@ export async function findTracksWithGenre( const param: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 10, + limit: searchResultLimit, offset: 0, }; const result = await search(param, context.service); @@ -647,7 +650,7 @@ export async function findTracks( const param: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", - limit: 10, + limit: searchResultLimit, offset: 0, }; const result = await search(param, context.service);