bridgething
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);
}

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);
}

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);
}

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);
}

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;
};