Loading...
Loading...
MANDATORY recipe for every Caffeine build that reads Spotify catalog data or drives a user's Spotify account from a canister. The supported path is the `spotify-client` mops package (Spotify Web API) over outbound HTTPS with an OAuth 2.0 bearer token minted off-chain. Hand-rolling `ic.http_request` calls to `api.spotify.com` is a FORBIDDEN anti-pattern — it bypasses the non-replicated-outcall safeguard (player state and `progress_ms` differ per node and fail consensus), the generated JSON decoding, and the bearer handling. Load this skill whenever the user, spec, or any prior task mentions Spotify, a song, track, artist, album, playlist, music search, new releases, genres, markets, "what's playing", recently played, the queue, player controls (play / pause / skip / shuffle / repeat), a saved library, podcasts (shows or episodes), chapters or audiobooks — and BEFORE writing any code that touches a Spotify endpoint.
npx skill4agent add caffeinelabs/skills connector-spotifyspotify-clientmo:ic/Typesic[toolchain]import { getTrack } "mo:spotify-client/Apis/TracksApi";
import { getInformationAboutTheUsersCurrentPlayback; skipUsersPlaybackToNextTrack } "mo:spotify-client/Apis/PlayerApi";
import { type Config; defaultConfig } "mo:spotify-client/Config";
persistent actor {
func config(accessToken : Text) : Config = { defaultConfig with auth = ?#bearer accessToken };
// Catalog read — a client-credentials token suffices. `market = ""` omits the
// parameter; empty strings and zeroes are how optional query params are
// dropped, there is no `?Text` to leave null.
public func trackName(accessToken : Text, id : Text) : async ?Text {
let track = await* getTrack(config accessToken, id, "");
track.name;
};
// User read — needs a user token with `user-read-playback-state`. An idle
// player answers 204 with an empty body, which the generated decoder cannot
// parse, so it rejects; treat that as "nothing playing", not as an error.
public func nowPlaying(accessToken : Text) : async ?Bool {
try {
let state = await* getInformationAboutTheUsersCurrentPlayback(config accessToken, "", "");
state.is_playing;
} catch (_err) {
null;
};
};
// User write — needs `user-modify-playback-state`. Returns `()`, and since
// 0.3.0 a non-2xx status rejects, so a missing scope or an expired token
// surfaces here instead of looking like success.
public func skip(accessToken : Text) : async () {
await* skipUsersPlaybackToNextTrack(config accessToken, "");
};
}searchgetTrackgetAnAlbumgetAnArtistgetNewReleasesgetCategoriesgetAvailableMarkets/me/*/me/*user-read-playback-stateuser-modify-playback-stateplaylist-modify-publicuser-library-readis_replicated = ?falsedefaultConfigPUTDELETEPOSTaddToQueueGETtimestampprogress_msis_replicated = ?truetransformis_replicated = nulllet userCfg = { cfg with is_replicated = null }nulladdToQueuedefaultConfigTrackObject.name : ?Text.artists : ?[SimplifiedArtistObject]CurrentlyPlayingContextObject.is_playing : ?Booldo ?switch11dFghVXANMlKmJXsNCbNlspotify:track:11dFghVXANMlKmJXsNCbNl/?urisspotify:track:…PlayerApi.startAUsersPlaybackcontext_uri : ?Texturis : ?[Text]position_ms : ?InttransferAUsersPlaybackplay : ?Booluriscontext_urioffset?Map<Text, Text>{"uri": "spotify:track:…"}{"position": 5}{"position": "5"}PlaylistsApi.uploadCustomPlaylistCoverimage/jpegformat: byteContent-Type: application/jsonBlobic.http_requestcaffeinelabs/skills-internaloneOfPlayerApi.getInformationAboutTheUsersCurrentPlaybackgetTheUsersCurrentlyPlayingTrackError.reject(… Failed to parse JSON …)trycatch'204': Playback not available or active?T?CurrentlyPlayingContextObjectasync* ()skippauseaddToQueuetrycatchdefaultConfig.cycles = 30_000_000_000searchlimit=50getAnAlbumgetAudioAnalysiscycles = 100_000_000_000max_response_bytes = ?2_000_000throw Error.reject(…)diagnosticsHTTP <status> body[<n>B]=<first 100 chars>: <reason>trycatchError.message(err)