Surfaces ↓
Library
client.library Daemon -> webapp replies and events for the library surface. BrowseReply, SearchReply,
RecommendationsReply, FavoritesListReply, and FavoritesContainsReply answer the
matching ClientToBridgeLibraryMsg request; LibraryErrorReply replaces the reply on
failure. FavoriteChanged is a live event broadcast to every connected webapp whenever a
favorite's status changes.
Requests
you ask, the daemon answers; await the tagged result and check .ok
browse(LibraryBrowse): Promise<TypedRequestResult<LibraryBrowseReply, LibraryErrorReply>>
push form: onBrowseReply (subscribe instead of awaiting)
Payload for the browse request: page through a folder in the library tree, or the root
menu when node_id is None. Root-level results are cached by the daemon for up to 5
minutes, so a fresh call immediately after a library change on the gateway side may still
return the previous shelf layout.
const res = await client.library.browse({ limit: 0, offset: 0 });
if (res.ok) {
console.log(res.response.result);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: LibraryBrowseReply }
| { ok: false; kind: 'domain'; error: LibraryErrorReply }
| { ok: false; kind: 'protocol'; error: WireError }; search(LibrarySearch): Promise<TypedRequestResult<LibrarySearchReply, LibraryErrorReply>>
push form: onSearchReply (subscribe instead of awaiting)
Payload for the search request: free-text search across the connected gateway's library.
const res = await client.library.search({ query: '...', limit: 0, offset: 0 });
if (res.ok) {
console.log(res.response.result);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: LibrarySearchReply }
| { ok: false; kind: 'domain'; error: LibraryErrorReply }
| { ok: false; kind: 'protocol'; error: WireError }; recommendations(LibraryRecommendations): Promise<TypedRequestResult<LibraryRecommendationsReply, LibraryErrorReply>>
push form: onRecommendationsReply (subscribe instead of awaiting)
Recommendations seeded by up to 5 items. Spotify hard-caps at 5 combined seeds across tracks/artists/genres.
const res = await client.library.recommendations({ seeds: [], limit: 0, offset: 0 });
if (res.ok) {
console.log(res.response.result);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: LibraryRecommendationsReply }
| { ok: false; kind: 'domain'; error: LibraryErrorReply }
| { ok: false; kind: 'protocol'; error: WireError }; favoritesList(LibraryFavoritesList): Promise<TypedRequestResult<LibraryFavoritesListReply, LibraryErrorReply>>
push form: onFavoritesListReply (subscribe instead of awaiting)
Payload for the favoritesList request: page through the user's saved/liked library items,
mixed across kinds.
const res = await client.library.favoritesList({ limit: 0, offset: 0 });
if (res.ok) {
console.log(res.response.page);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: LibraryFavoritesListReply }
| { ok: false; kind: 'domain'; error: LibraryErrorReply }
| { ok: false; kind: 'protocol'; error: WireError }; favoritesContains(LibraryFavoritesContains): Promise<TypedRequestResult<LibraryFavoritesContainsReply, LibraryErrorReply>>
push form: onFavoritesContainsReply (subscribe instead of awaiting)
Batch "is each of these favorited?" lookup. Reply liked is
index-aligned with the request uris, capped at 50 per call.
const res = await client.library.favoritesContains({ uris: [] });
if (res.ok) {
console.log(res.response.liked);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: LibraryFavoritesContainsReply }
| { ok: false; kind: 'domain'; error: LibraryErrorReply }
| { ok: false; kind: 'protocol'; error: WireError }; Commands
fire-and-forget; the promise resolves once the daemon has taken the message
favoritesToggle(FavoritesToggle): Promise<void>
await client.library.favoritesToggle({ item: { /* ItemRef */ } }); favoritesSet(FavoritesSet): Promise<void>
await client.library.favoritesSet({ item: { /* ItemRef */ }, liked: true }); favoritesSetMany(FavoritesSetMany): Promise<void>
await client.library.favoritesSetMany({ entries: [] }); Events
the daemon pushes these unprompted; subscribing returns an unsubscribe function
onFavoriteChanged(handler: (FavoriteChanged) => void): () => void
const off = client.library.onFavoriteChanged((favoriteChanged) => {
console.log(favoriteChanged.uri);
});
// call off() to unsubscribe Types
shapes referenced above, as the SDK types them
Fired when the favorited / liked status of an item changes - regardless of whether it was driven by the daemon (FavoritesToggle/Set command) or by the user mutating it on the gateway-side app directly.
type FavoriteChanged = {
uri: string;
liked: boolean;
}; Payload for the browse request: page through a folder in the library tree, or the root
menu when node_id is None. Root-level results are cached by the daemon for up to 5
minutes, so a fresh call immediately after a library change on the gateway side may still
return the previous shelf layout.
type LibraryBrowse = {
nodeId?: string; // Folder to descend into, from a prior result's `BrowseFolder::node_id`. `None` browses the root.
limit: number; // Requested page size; the daemon clamps this to 100 regardless of the value sent.
offset: number;
sections?: number; // Root only: cap on the number of folders returned. `None` returns every folder.
preview?: number; // Root only: preview children per folder. `None` is the gateway default; `0` skips preview hydration entirely and returns a cheap index of node ids, titles, and totals.
}; type LibraryBrowseReply = {
result: BrowseResult;
}; type LibraryErrorReply = {
error: LibraryError;
}; Payload for the search request: free-text search across the connected gateway's library.
type LibrarySearch = {
query: string;
kinds?: ItemKind[]; // Restrict results to these item kinds. `None` searches every kind.
limit: number; // Requested page size; the daemon clamps this to 100 regardless of the value sent.
offset: number;
}; type LibrarySearchReply = {
result: SearchResult;
}; Recommendations seeded by up to 5 items. Spotify hard-caps at 5 combined seeds across tracks/artists/genres.
type LibraryRecommendations = {
seeds: ItemRef[]; // Seed items; the daemon truncates this to the first 5 regardless of the count sent.
kind?: ItemKind; // Restrict results to this item kind. `None` lets the gateway choose based on the seeds.
limit: number; // Requested page size; the daemon clamps this to 100 regardless of the value sent.
offset: number;
}; type LibraryRecommendationsReply = {
result: RecommendationsResult;
}; Payload for the favoritesList request: page through the user's saved/liked library items,
mixed across kinds.
type LibraryFavoritesList = {
limit: number; // Requested page size; the daemon clamps this to 100 regardless of the value sent.
offset: number;
}; type LibraryFavoritesListReply = {
page: FavoritesPage;
}; Batch "is each of these favorited?" lookup. Reply liked is
index-aligned with the request uris, capped at 50 per call.
type LibraryFavoritesContains = {
uris: string[]; // Uris to check; the daemon truncates this to the first 50 regardless of the count sent.
}; type LibraryFavoritesContainsReply = {
liked: boolean[]; // Index-aligned with the request's `uris`.
}; type FavoritesToggle = {
item: ItemRef;
}; type FavoritesSet = {
item: ItemRef;
liked: boolean;
}; Bulk favorites mutation. entries are independent FavoritesSet
applications; gateway returns once it has issued each underlying
platform call. Per-entry errors are not surfaced - companion logs
and best-efforts the rest. Webapps observing partial success listen
for FavoriteChanged events.
type FavoritesSetMany = {
entries: FavoritesSet[];
}; Page of browse results. total is the count of items in the
underlying collection when the gateway can cheaply expose it (None
means indeterminate). has_more is the authoritative end-of-data
signal - webapps paginate by raising offset until has_more is
false rather than relying on total.
type BrowseResult = {
entries: BrowseEntry[];
total?: number;
hasMore: boolean;
}; type LibraryError =
| 'notFound' // The named uri or node id does not exist in the gateway's library.
| 'notSupported' // The operation isn't supported by the underlying source (e.g. a platform that exposes browse but not recommendations).
| 'unauthorized' // User account / OAuth scope does not permit the operation.
| 'noGateway' // No companion is connected to back the surface.; Coarse type tag a webapp uses to filter or branch. Mirrors the variant
names of LibraryItem; kept separate so search/recommendations can
constrain by kind without having to construct a sample item.
type ItemKind =
| 'track'
| 'album'
| 'playlist'
| 'podcastEpisode'
| 'show'
| 'artist'
| 'station'; Page of search results. kinds is the constrained kinds the search
honored (echoed back so webapps can detect ignored constraints); items
are ranked best-first.
type SearchResult = {
items: LibraryItem[];
kinds: ItemKind[];
total?: number;
hasMore: boolean;
}; Stable URI + kind a webapp passes back to act on a library item
(e.g. player.play({ uri }), library.favorites.toggle({ item })).
persistent_id is the platform-stable id when the gateway has one;
webapps treat it as opaque.
type ItemRef = {
uri: string;
kind: ItemKind;
persistentId?: string;
}; Page of recommendation results. Gateway decides how seed + kind interact (Spotify uses radio-style seeding, Apple Music uses curated rails) - the daemon doesn't prescribe.
type RecommendationsResult = {
items: LibraryItem[];
total?: number;
hasMore: boolean;
}; Page of the user's favorited / liked / saved library items. Mixed-kind because most platforms expose one "Saved" surface across kinds.
type FavoritesPage = {
items: LibraryItem[];
total?: number;
hasMore: boolean;
}; One row in a BrowseResult: either a folder (drilldown) or a leaf
item the user can play / queue / favorite.
type BrowseEntry =
| { type: 'folder'; data: BrowseFolder }
| { type: 'item'; data: LibraryItem }; One playable / browsable item from the library. Lean per-variant payload - gateways translate platform-specific extras down to these fields, rare per-platform fields just don't surface. Forward-compat: adding new variants or fields is an additive change webapps can branch on.
type LibraryItem =
| { type: 'track'; data: Track }
| { type: 'album'; data: Album }
| { type: 'playlist'; data: Playlist }
| { type: 'podcastEpisode'; data: PodcastEpisode }
| { type: 'show'; data: Show }
| { type: 'artist'; data: Artist }
| { type: 'station'; data: Station }; A drilldown node. node_id is opaque and gateway-defined; webapps
pass it back as the next browse({ node_id }) to descend. total is
the count of children behind this folder when the gateway can cheaply
expose it. preview_children is an inline first-N slice of those
children so home-shelf shapes don't need a separate drill round-trip;
gateways populate it when cheap (Spotify Web API home shelves include
previews; Apple Music curated rails do too) and leave it None
otherwise.
type BrowseFolder = {
nodeId: string;
title: string;
subtitle?: string;
artworkId?: string;
total?: number;
previewChildren?: BrowseEntry[];
}; A track with resolved album/artist metadata, used by library search
and browse results. For live now-playing state see MediaItem.
type Track = {
id: string;
name: string;
album: Album;
artist: Artist; // Primary credited artist.
artists: Artist[]; // All credited artists, in order.
durationMs: number;
imageId: string; // Opaque artwork asset id; pass to `asset.get` for the bytes.
saved: boolean; // Whether the track is saved in the user's library.
}; An album reference.
type Album = {
id: string;
name: string;
artworkId?: string; // Opaque artwork asset id (`asset.get`), when known.
}; Lean cross-platform shape for a playlist. uri is what player.play
would route on; track_count is best-effort (some sources don't expose
it cheaply); owner_name is whatever the source surfaces (Spotify
owner, Apple Music curator, etc.).
type Playlist = {
uri: string;
name: string;
ownerName?: string;
trackCount?: number;
artworkId?: string;
}; One episode of a podcast. show_name mirrors what the gateway exposes
at episode-level so a webapp can render show + episode without a
separate fetch. published_at_ms is best-effort; not every gateway
surfaces it.
type PodcastEpisode = {
uri: string;
name: string;
showName?: string;
durationMs?: number;
publishedAtUnixS?: number;
artworkId?: string;
}; One podcast show (parent of PodcastEpisode). episode_count is
best-effort.
type Show = {
uri: string;
name: string;
publisher?: string;
episodeCount?: number;
artworkId?: string;
}; An artist reference.
type Artist = {
id: string;
name: string;
artworkId?: string; // Opaque artwork asset id (`asset.get`), when known.
}; Algorithmic / radio station. seed is the URI the station was seeded
from when known (artist, track, etc.).
type Station = {
uri: string;
name: string;
seed?: string;
artworkId?: string;
};