# Get albums for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-albums-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorAlbums # Get artists for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-artists-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorArtists # Get compatibility for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-compatibility-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorCompatibility # Get loved songs for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-loved-songs-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorLovedSongs # Get neighbours for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-neighbours-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorNeighbours # Get playlists for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-playlists-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorPlaylists # Get scrobbles for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-scrobbles-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorScrobbles # Get songs for an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-songs-for-an-actor /api-reference/openapi.json get /app.rocksky.actor.getActorSongs # Get the profile of an actor Source: https://docs.rocksky.app/api-reference/approckskyactor/get-the-profile-of-an-actor /api-reference/openapi.json get /app.rocksky.actor.getProfile # Get albums Source: https://docs.rocksky.app/api-reference/approckskyalbum/get-albums /api-reference/openapi.json get /app.rocksky.album.getAlbums # Get detailed album view Source: https://docs.rocksky.app/api-reference/approckskyalbum/get-detailed-album-view /api-reference/openapi.json get /app.rocksky.album.getAlbum # Get tracks for an album Source: https://docs.rocksky.app/api-reference/approckskyalbum/get-tracks-for-an-album /api-reference/openapi.json get /app.rocksky.album.getAlbumTracks # Create a new API key for the authenticated user Source: https://docs.rocksky.app/api-reference/approckskyapikey/create-a-new-api-key-for-the-authenticated-user /api-reference/openapi.json post /app.rocksky.apikey.createApikey # Get a list of API keys for the authenticated user Source: https://docs.rocksky.app/api-reference/approckskyapikey/get-a-list-of-api-keys-for-the-authenticated-user /api-reference/openapi.json get /app.rocksky.apikey.getApikeys # Remove an API key for the authenticated user Source: https://docs.rocksky.app/api-reference/approckskyapikey/remove-an-api-key-for-the-authenticated-user /api-reference/openapi.json post /app.rocksky.apikey.removeApikey # Update an existing API key for the authenticated user Source: https://docs.rocksky.app/api-reference/approckskyapikey/update-an-existing-api-key-for-the-authenticated-user /api-reference/openapi.json post /app.rocksky.apikey.updateApikey # Get artist details Source: https://docs.rocksky.app/api-reference/approckskyartist/get-artist-details /api-reference/openapi.json get /app.rocksky.artist.getArtist # Get artist listeners Source: https://docs.rocksky.app/api-reference/approckskyartist/get-artist-listeners /api-reference/openapi.json get /app.rocksky.artist.getArtistListeners # Get artist recent listeners ordered by most recent scrobble Source: https://docs.rocksky.app/api-reference/approckskyartist/get-artist-recent-listeners-ordered-by-most-recent-scrobble /api-reference/openapi.json get /app.rocksky.artist.getArtistRecentListeners # Get artists Source: https://docs.rocksky.app/api-reference/approckskyartist/get-artists /api-reference/openapi.json get /app.rocksky.artist.getArtists # Get artist's albums Source: https://docs.rocksky.app/api-reference/approckskyartist/get-artists-albums /api-reference/openapi.json get /app.rocksky.artist.getArtistAlbums # Get artist's tracks Source: https://docs.rocksky.app/api-reference/approckskyartist/get-artists-tracks /api-reference/openapi.json get /app.rocksky.artist.getArtistTracks # Get the scrobbles chart Source: https://docs.rocksky.app/api-reference/approckskycharts/get-the-scrobbles-chart /api-reference/openapi.json get /app.rocksky.charts.getScrobblesChart # Get top artists Source: https://docs.rocksky.app/api-reference/approckskycharts/get-top-artists /api-reference/openapi.json get /app.rocksky.charts.getTopArtists # Get top tracks Source: https://docs.rocksky.app/api-reference/approckskycharts/get-top-tracks /api-reference/openapi.json get /app.rocksky.charts.getTopTracks # Get all currently playing tracks by users Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-all-currently-playing-tracks-by-users /api-reference/openapi.json get /app.rocksky.feed.getStories # Get all feed generators Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-all-feed-generators /api-reference/openapi.json get /app.rocksky.feed.getFeedGenerators # Get information about a feed generator Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-information-about-a-feed-generator /api-reference/openapi.json get /app.rocksky.feed.describeFeedGenerator # Get information about a feed generator Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-information-about-a-feed-generator-1 /api-reference/openapi.json get /app.rocksky.feed.getFeedGenerator # Get personalised album recommendations for a user Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-personalised-album-recommendations-for-a-user /api-reference/openapi.json get /app.rocksky.feed.getAlbumRecommendations # Get personalised artist recommendations for a user Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-personalised-artist-recommendations-for-a-user /api-reference/openapi.json get /app.rocksky.feed.getArtistRecommendations # Get personalised track recommendations for a user Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-personalised-track-recommendations-for-a-user /api-reference/openapi.json get /app.rocksky.feed.getRecommendations # Get the feed by uri Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-the-feed-by-uri /api-reference/openapi.json get /app.rocksky.feed.getFeed # Get the feed by uri Source: https://docs.rocksky.app/api-reference/approckskyfeed/get-the-feed-by-uri-1 /api-reference/openapi.json get /app.rocksky.feed.getFeedSkeleton # Search for content in the feed Source: https://docs.rocksky.app/api-reference/approckskyfeed/search-for-content-in-the-feed /api-reference/openapi.json get /app.rocksky.feed.search # Creates a 'follow' relationship from the authenticated account to a specified account. Source: https://docs.rocksky.app/api-reference/approckskygraph/creates-a-follow-relationship-from-the-authenticated-account-to-a-specified-account /api-reference/openapi.json post /app.rocksky.graph.followAccount # Enumerates accounts which a specified account (actor) follows. Source: https://docs.rocksky.app/api-reference/approckskygraph/enumerates-accounts-which-a-specified-account-actor-follows /api-reference/openapi.json get /app.rocksky.graph.getFollows # Enumerates accounts which follow a specified account (actor). Source: https://docs.rocksky.app/api-reference/approckskygraph/enumerates-accounts-which-follow-a-specified-account-actor /api-reference/openapi.json get /app.rocksky.graph.getFollowers # Enumerates accounts which follow a specified account (actor) and are followed by the viewer. Source: https://docs.rocksky.app/api-reference/approckskygraph/enumerates-accounts-which-follow-a-specified-account-actor-and-are-followed-by-the-viewer /api-reference/openapi.json get /app.rocksky.graph.getKnownFollowers # Removes a 'follow' relationship from the authenticated account to a specified account. Source: https://docs.rocksky.app/api-reference/approckskygraph/removes-a-follow-relationship-from-the-authenticated-account-to-a-specified-account /api-reference/openapi.json post /app.rocksky.graph.unfollowAccount # Dislike a shout Source: https://docs.rocksky.app/api-reference/approckskylike/dislike-a-shout /api-reference/openapi.json post /app.rocksky.like.dislikeShout # Dislike a song Source: https://docs.rocksky.app/api-reference/approckskylike/dislike-a-song /api-reference/openapi.json post /app.rocksky.like.dislikeSong # Like a shout Source: https://docs.rocksky.app/api-reference/approckskylike/like-a-shout /api-reference/openapi.json post /app.rocksky.like.likeShout # Like a song Source: https://docs.rocksky.app/api-reference/approckskylike/like-a-song /api-reference/openapi.json post /app.rocksky.like.likeSong # Get the authenticated user's scrobble mirror sources (Last.fm, ListenBrainz, Teal.fm). Source: https://docs.rocksky.app/api-reference/approckskymirror/get-the-authenticated-users-scrobble-mirror-sources-lastfm-listenbrainz-tealfm /api-reference/openapi.json get /app.rocksky.mirror.getMirrorSources # Upsert a mirror source for the authenticated user. Toggling `enabled` notifies the mirror process over NATS so it can start/stop the per-user task without a restart. Source: https://docs.rocksky.app/api-reference/approckskymirror/upsert-a-mirror-source-for-the-authenticated-user-toggling-`enabled`-notifies-the-mirror-process-over-nats-so-it-can-startstop-the-per-user-task-without-a-restart /api-reference/openapi.json post /app.rocksky.mirror.putMirrorSource # Add directory to the player's queue Source: https://docs.rocksky.app/api-reference/approckskyplayer/add-directory-to-the-players-queue /api-reference/openapi.json post /app.rocksky.player.addDirectoryToQueue # Add items to the player's queue Source: https://docs.rocksky.app/api-reference/approckskyplayer/add-items-to-the-players-queue /api-reference/openapi.json post /app.rocksky.player.addItemsToQueue # Get the currently playing track Source: https://docs.rocksky.app/api-reference/approckskyplayer/get-the-currently-playing-track /api-reference/openapi.json get /app.rocksky.player.getCurrentlyPlaying # Pause the currently playing track Source: https://docs.rocksky.app/api-reference/approckskyplayer/pause-the-currently-playing-track /api-reference/openapi.json post /app.rocksky.player.pause # Play a specific audio file Source: https://docs.rocksky.app/api-reference/approckskyplayer/play-a-specific-audio-file /api-reference/openapi.json post /app.rocksky.player.playFile # Play all tracks in a directory Source: https://docs.rocksky.app/api-reference/approckskyplayer/play-all-tracks-in-a-directory /api-reference/openapi.json post /app.rocksky.player.playDirectory # Play the next track in the queue Source: https://docs.rocksky.app/api-reference/approckskyplayer/play-the-next-track-in-the-queue /api-reference/openapi.json post /app.rocksky.player.next # Play the previous track in the queue Source: https://docs.rocksky.app/api-reference/approckskyplayer/play-the-previous-track-in-the-queue /api-reference/openapi.json post /app.rocksky.player.previous # Resume playback of the currently paused track Source: https://docs.rocksky.app/api-reference/approckskyplayer/resume-playback-of-the-currently-paused-track /api-reference/openapi.json post /app.rocksky.player.play # Retrieve the current playback queue Source: https://docs.rocksky.app/api-reference/approckskyplayer/retrieve-the-current-playback-queue /api-reference/openapi.json get /app.rocksky.player.getPlaybackQueue # Seek to a specific position in the currently playing track Source: https://docs.rocksky.app/api-reference/approckskyplayer/seek-to-a-specific-position-in-the-currently-playing-track /api-reference/openapi.json post /app.rocksky.player.seek # Create a new playlist Source: https://docs.rocksky.app/api-reference/approckskyplaylist/create-a-new-playlist /api-reference/openapi.json post /app.rocksky.playlist.createPlaylist # Insert a directory into a playlist Source: https://docs.rocksky.app/api-reference/approckskyplaylist/insert-a-directory-into-a-playlist /api-reference/openapi.json post /app.rocksky.playlist.insertDirectory # Insert files into a playlist Source: https://docs.rocksky.app/api-reference/approckskyplaylist/insert-files-into-a-playlist /api-reference/openapi.json post /app.rocksky.playlist.insertFiles # Remove a playlist Source: https://docs.rocksky.app/api-reference/approckskyplaylist/remove-a-playlist /api-reference/openapi.json post /app.rocksky.playlist.removePlaylist # Remove a track from a playlist Source: https://docs.rocksky.app/api-reference/approckskyplaylist/remove-a-track-from-a-playlist /api-reference/openapi.json post /app.rocksky.playlist.removeTrack # Retrieve a list of playlists Source: https://docs.rocksky.app/api-reference/approckskyplaylist/retrieve-a-list-of-playlists /api-reference/openapi.json get /app.rocksky.playlist.getPlaylists # Retrieve a playlist by its ID Source: https://docs.rocksky.app/api-reference/approckskyplaylist/retrieve-a-playlist-by-its-id /api-reference/openapi.json get /app.rocksky.playlist.getPlaylist # Start a playlist Source: https://docs.rocksky.app/api-reference/approckskyplaylist/start-a-playlist /api-reference/openapi.json post /app.rocksky.playlist.startPlaylist # Create a new scrobble Source: https://docs.rocksky.app/api-reference/approckskyscrobble/create-a-new-scrobble /api-reference/openapi.json post /app.rocksky.scrobble.createScrobble # Get a scrobble by its unique identifier Source: https://docs.rocksky.app/api-reference/approckskyscrobble/get-a-scrobble-by-its-unique-identifier /api-reference/openapi.json get /app.rocksky.scrobble.getScrobble # Get scrobbles all scrobbles Source: https://docs.rocksky.app/api-reference/approckskyscrobble/get-scrobbles-all-scrobbles /api-reference/openapi.json get /app.rocksky.scrobble.getScrobbles # Create a new shout Source: https://docs.rocksky.app/api-reference/approckskyshout/create-a-new-shout /api-reference/openapi.json post /app.rocksky.shout.createShout # Get all shouts for a specific track Source: https://docs.rocksky.app/api-reference/approckskyshout/get-all-shouts-for-a-specific-track /api-reference/openapi.json get /app.rocksky.shout.getTrackShouts # Get replies to a shout Source: https://docs.rocksky.app/api-reference/approckskyshout/get-replies-to-a-shout /api-reference/openapi.json get /app.rocksky.shout.getShoutReplies # Get shouts for an album Source: https://docs.rocksky.app/api-reference/approckskyshout/get-shouts-for-an-album /api-reference/openapi.json get /app.rocksky.shout.getAlbumShouts # Get shouts for an artist Source: https://docs.rocksky.app/api-reference/approckskyshout/get-shouts-for-an-artist /api-reference/openapi.json get /app.rocksky.shout.getArtistShouts # Get the shouts of an actor's profile Source: https://docs.rocksky.app/api-reference/approckskyshout/get-the-shouts-of-an-actors-profile /api-reference/openapi.json get /app.rocksky.shout.getProfileShouts # Remove a shout by its ID Source: https://docs.rocksky.app/api-reference/approckskyshout/remove-a-shout-by-its-id /api-reference/openapi.json post /app.rocksky.shout.removeShout # Reply to a shout Source: https://docs.rocksky.app/api-reference/approckskyshout/reply-to-a-shout /api-reference/openapi.json post /app.rocksky.shout.replyShout # Report a shout for moderation Source: https://docs.rocksky.app/api-reference/approckskyshout/report-a-shout-for-moderation /api-reference/openapi.json post /app.rocksky.shout.reportShout # Create a new song Source: https://docs.rocksky.app/api-reference/approckskysong/create-a-new-song /api-reference/openapi.json post /app.rocksky.song.createSong # Get a song by its uri, MusicBrainz ID, or ISRC Source: https://docs.rocksky.app/api-reference/approckskysong/get-a-song-by-its-uri-musicbrainz-id-or-isrc /api-reference/openapi.json get /app.rocksky.song.getSong # Get song recent listeners ordered by most recent scrobble Source: https://docs.rocksky.app/api-reference/approckskysong/get-song-recent-listeners-ordered-by-most-recent-scrobble /api-reference/openapi.json get /app.rocksky.song.getSongRecentListeners # Get songs Source: https://docs.rocksky.app/api-reference/approckskysong/get-songs /api-reference/openapi.json get /app.rocksky.song.getSongs # Matches a song against Rocksky’s music database and external metadata providers to resolve the best canonical track, artist, and album Source: https://docs.rocksky.app/api-reference/approckskysong/matches-a-song-against-rocksky’s-music-database-and-external-metadata-providers-to-resolve-the-best-canonical-track-artist-and-album /api-reference/openapi.json get /app.rocksky.song.matchSong # Get the currently playing track Source: https://docs.rocksky.app/api-reference/approckskyspotify/get-the-currently-playing-track /api-reference/openapi.json get /app.rocksky.spotify.getCurrentlyPlaying # Pause the currently playing track Source: https://docs.rocksky.app/api-reference/approckskyspotify/pause-the-currently-playing-track /api-reference/openapi.json post /app.rocksky.spotify.pause # Play the next track in the queue Source: https://docs.rocksky.app/api-reference/approckskyspotify/play-the-next-track-in-the-queue /api-reference/openapi.json post /app.rocksky.spotify.next # Play the previous track in the queue Source: https://docs.rocksky.app/api-reference/approckskyspotify/play-the-previous-track-in-the-queue /api-reference/openapi.json post /app.rocksky.spotify.previous # Resume playback of the currently paused track Source: https://docs.rocksky.app/api-reference/approckskyspotify/resume-playback-of-the-currently-paused-track /api-reference/openapi.json post /app.rocksky.spotify.play # Seek to a specific position in the currently playing track Source: https://docs.rocksky.app/api-reference/approckskyspotify/seek-to-a-specific-position-in-the-currently-playing-track /api-reference/openapi.json post /app.rocksky.spotify.seek # Get a user's year-in-review Wrapped stats Source: https://docs.rocksky.app/api-reference/approckskystats/get-a-users-year-in-review-wrapped-stats /api-reference/openapi.json get /app.rocksky.stats.getWrapped # Get approckskystatsgetstats Source: https://docs.rocksky.app/api-reference/approckskystats/get-approckskystatsgetstats /api-reference/openapi.json get /app.rocksky.stats.getStats # Introduction Source: https://docs.rocksky.app/api-reference/introduction The Rocksky HTTP API, built on AT Protocol XRPC. The Rocksky API is an XRPC ([AT Protocol](https://atproto.com)) API. Every endpoint lives under the `app.rocksky.*` namespace and is reachable at `https://api.rocksky.app/xrpc/`. ## Base URL ``` https://api.rocksky.app ``` ## Authentication Authenticated endpoints expect a **bearer token** in the `Authorization` header: ``` Authorization: Bearer ``` You can obtain a token by: * Signing in at [rocksky.app](https://rocksky.app) and copying the OAuth callback JWT * Running [`rocksky login `](/cli/login) with the CLI * Creating an [API key](/cli/create-apikey) for an application * Creating a personal **Access Token** at [rocksky.app/access-tokens](https://rocksky.app/access-tokens) — name it, copy the secret once, and use it interchangeably with a JWT (see [Personal access tokens](#personal-access-tokens) below) Read-only endpoints (profiles, charts, public scrobble feeds, search) work without authentication. ## Personal access tokens Access tokens are long-lived JWTs you generate from the web UI for scripts, CLIs, and integrations that can't run an interactive OAuth flow. They behave exactly like an OAuth-issued JWT — pass them in the `Authorization: Bearer` header on any authenticated endpoint. Create / list / revoke them at: * Web: [rocksky.app/access-tokens](https://rocksky.app/access-tokens) * Mobile web: [m.rocksky.app/access-tokens](https://m.rocksky.app/access-tokens) The full secret is shown **once**, at creation — copy it somewhere safe. Subsequent listings only show the last four characters. Deleting a token revokes it immediately on the API side (no waiting for expiry). ```http theme={null} GET https://api.rocksky.app/access-tokens Authorization: Bearer POST https://api.rocksky.app/access-tokens Authorization: Bearer Content-Type: application/json { "name": "my-laptop" } DELETE https://api.rocksky.app/access-tokens/{id} Authorization: Bearer ``` ## Audioscrobbler endpoints For Last.fm / ListenBrainz compatibility, Rocksky exposes a separate host: ``` https://audioscrobbler.rocksky.app ``` * [Migrate from Last.fm](/migrations/from-lastfm) * [Migrate from ListenBrainz](/migrations/from-listenbrainz) ## SDKs Prefer typed bindings? Use one of the [official SDKs](/sdks/overview) in TypeScript, Python, Rust, Go, Ruby, Kotlin, Elixir, Clojure, or Gleam — every endpoint in this reference is wrapped. ## Errors Non-2xx responses return an XRPC-style error body: ```json theme={null} { "error": "InvalidRequest", "message": "actor must be a DID or handle" } ``` The HTTP status code carries the category (400-class for client errors, 500-class for server errors). ## OpenAPI spec The endpoint reference below is generated directly from the [OpenAPI specification](/api-reference/openapi.json) shipped with this site. # albums Source: https://docs.rocksky.app/cli/albums Show top albums for a user. List a user's top albums, ranked by play count. ## Usage ```bash theme={null} rocksky albums [options] [did] ``` ## Parameters | Name | Required | Description | | ------- | -------- | ------------------------------------------------------ | | `[did]` | No | DID to inspect. Defaults to the authenticated session. | ## Example ```bash theme={null} rocksky albums did:plc:7vdlgi2bflelz7mmuxoqjfcr ``` # artists Source: https://docs.rocksky.app/cli/artists Show top artists for a user. List a user's top artists, ranked by play count. ## Usage ```bash theme={null} rocksky artists [options] [did] ``` ## Parameters | Name | Required | Description | | ------- | -------- | ------------------------------------------------------ | | `[did]` | No | DID to inspect. Defaults to the authenticated session. | ## Example ```bash theme={null} rocksky artists did:plc:7vdlgi2bflelz7mmuxoqjfcr ``` # create apikey Source: https://docs.rocksky.app/cli/create-apikey Generate a new API key for an application or integration. Create a Rocksky API key. Use it with the [scrobble API](/migrations/from-listenbrainz), the [SDKs](/sdks/overview), or any third-party scrobbler. ## Usage ```bash theme={null} rocksky create apikey [options] ``` ## Parameters | Name | Required | Description | | -------- | -------- | ------------------------------- | | `` | Yes | Human-readable name for the key | ## Options | Flag | Description | | ----------- | --------------------------------------- | | `-d ` | Description of what the key is used for | ## Example ```bash theme={null} rocksky create apikey -d "Used for my scrobbling bot" scrobbler-bot ``` # help Source: https://docs.rocksky.app/cli/help Show help for any rocksky command. Display usage info for the CLI or a specific subcommand. ## Usage ```bash theme={null} rocksky help [command] ``` ## Examples ```bash theme={null} rocksky help rocksky help scrobble rocksky help create apikey ``` # import Source: https://docs.rocksky.app/cli/import Import your listening history from a Spotify or Last.fm export. Bulk-import your past listens into Rocksky from a **Spotify Extended Streaming History** or **Last.fm** export. The command autodetects the format, enriches each play with full metadata, and publishes it to your PDS as a scrobble — throttled to stay within Bluesky's write limits and safe to stop and resume. ## Usage ```bash theme={null} rocksky import [options] ``` `` is a Spotify or Last.fm export — a JSON/CSV file, or the whole **Spotify Extended Streaming History** folder (the format is autodetected from the contents). ## Import your history * **Spotify** — request your *Extended Streaming History* from [Privacy settings](https://www.spotify.com/account/privacy/). Spotify emails a `my_spotify_data.zip`; unzip it and keep the **Spotify Extended Streaming History** folder. * **Last.fm** — export your scrobbles as CSV or JSON with a tool such as [lastfm-to-csv](https://benjaminbenben.com/lastfm-to-csv/) or [ghan64's export](https://lastfm.ghan.nl/export/). `import` writes scrobbles to your repo on your PDS, so it needs to sign in. Use a **dedicated App Password** — never your main account password. 1. Sign in at [bsky.app](https://bsky.app). 2. Go to **Settings → Privacy and Security → App Passwords** ([direct link](https://bsky.app/settings/app-passwords)). 3. Click **Add App Password**, name it (e.g. `rocksky-import`), and copy the generated password (format `xxxx-xxxx-xxxx-xxxx`). An App Password is scoped and revocable — you can delete it any time without changing your main password. Do not use your account password here. `import` reads your credentials from the environment (or a `.env` file in the current directory): ```bash theme={null} export ROCKSKY_IDENTIFIER=alice.bsky.social # your handle or DID export ROCKSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx # the App Password from step 2 ``` | Variable | Required | Description | | -------------------- | -------- | ---------------------------------------------------- | | `ROCKSKY_IDENTIFIER` | Yes | Your Bluesky handle or DID | | `ROCKSKY_PASSWORD` | Yes | A Bluesky **App Password** (not your login password) | Both are **required** — the import aborts with an error if either is missing. Always dry-run first. This parses the export and prints exactly what would be published — **without writing anything** to your PDS and without needing your credentials: ```bash theme={null} rocksky import "Spotify Extended Streaming History" --dry-run ``` A dry run always previews the full import from the beginning, regardless of any earlier partial run. When the preview looks right, drop `--dry-run`: ```bash theme={null} rocksky import "Spotify Extended Streaming History" ``` The command logs each step — reading, authenticating, building a dedup index, then publishing — with a live progress bar showing the scrobble currently being written. A large history is throttled and can take a while; it is safe to stop with `Ctrl-C` and re-run to **resume exactly where it left off**. ## Options | Flag | Default | Description | | ------------------------- | ---------- | -------------------------------------------------------------------- | | `-f, --format ` | autodetect | Force the format: `spotify` or `lastfm` | | `-n, --dry-run` | off | Parse and print what would be published, without writing to your PDS | | `-c, --concurrency ` | `4` | Number of scrobbles to publish in parallel | | `-r, --rate ` | max safe | Scrobbles published per hour (hard-capped under the PDS write limit) | | `-l, --limit ` | all | Only import the first N scrobbles | | `--min-seconds ` | `30` | Skip Spotify plays shorter than this many seconds | | `--from ` | — | Only import scrobbles on or after this date (e.g. `2024-01-01`) | | `--to ` | — | Only import scrobbles on or before this date | | `--restart` | off | Ignore the saved checkpoint and import from the beginning | ## How it works * **Autodetection** — a Spotify export folder, a Spotify JSON file, or a Last.fm CSV/JSON file are all recognized automatically. Podcasts, audiobooks, and very short Spotify plays (under `--min-seconds`) are skipped and reported. * **Metadata enrichment** — the exports carry only title, artist, and album, so each play is matched against Rocksky's catalog to fill in duration, album art, MusicBrainz IDs, and streaming links before it is written. * **Rate limiting** — Bluesky rate-limits repo writes, and each scrobble also publishes its artist, album, and song records (deduplicated). The publish rate is **hard-capped** below that budget, so neither the default nor any `--rate` you pass can exceed the PDS write limit. * **Deduplication** — a local index of your existing repo is built first (and kept live off the firehose during the run), so re-imports never republish a scrobble you already have. * **Resumable** — a checkpoint tracks the last imported scrobble. Interrupt the import at any time and re-run the same command to continue from exactly where it stopped; use `--restart` to start over from the beginning. ## Examples ```bash theme={null} # Preview a Last.fm CSV export rocksky import scrobbles.csv --dry-run # Import only 2024 plays, forcing the Spotify parser rocksky import history.json --format spotify --from 2024-01-01 --to 2024-12-31 # Import the first 100 scrobbles as a small test run rocksky import "Spotify Extended Streaming History" --limit 100 ``` Prefer to point an existing app at Rocksky instead of a one-time import? See [Migrating from Last.fm](/migrations/from-lastfm) and [Migrating from ListenBrainz](/migrations/from-listenbrainz). # login Source: https://docs.rocksky.app/cli/login Authenticate the CLI with your Bluesky account. Sign in with a Bluesky handle. The CLI exchanges credentials for a session token used by every subsequent command. ## Usage ```bash theme={null} rocksky login ``` ## Parameters | Name | Required | Description | | ---------- | -------- | ---------------------------------------------- | | `` | Yes | Your Bluesky handle (e.g. `alice.bsky.social`) | ## Example ```bash theme={null} rocksky login tsiry-sandratraina.com ``` After login completes, run [`rocksky whoami`](/cli/whoami) to confirm the session is active. # mcp Source: https://docs.rocksky.app/cli/mcp Start an MCP server so Claude and other LLMs can read your Rocksky data. Start a [Model Context Protocol](https://modelcontextprotocol.io) server. Once registered with an MCP-capable client (Claude Desktop, Cursor, …), the LLM can call Rocksky tools to query your scrobbles, top artists, now playing, and more. ## Usage ```bash theme={null} rocksky mcp ``` ## Wire it into Claude Desktop Add this to your Claude Desktop MCP config: ```json theme={null} { "mcpServers": { "rocksky": { "command": "rocksky", "args": ["mcp"] } } } ``` See [Claude Desktop (MCP)](/integrations/claude-desktop) for the full setup walkthrough. # mpd Source: https://docs.rocksky.app/cli/mpd Serve your Rocksky library over the MPD protocol to control playback and browse music. Start an [MPD](https://www.musicpd.org/)-protocol server backed by your Rocksky library. Any MPD client — `ncmpcpp`, `mpc`, `Cantata`, mobile MPD apps — can then browse your music and control playback, with Rocksky handling the streaming and scrobbling behind the scenes. ## Usage ```bash theme={null} rocksky mpd [options] ``` ## Requirements Playback needs an authenticated session. Log in once first: ```bash theme={null} rocksky login ``` The server still starts if you are not logged in — it just prints a note and serves an empty library until you authenticate. Unlike [`import`](/cli/import) and [`scrobble-api`](/cli/scrobble-api), `mpd` uses your saved [`login`](/cli/login) session, not `ROCKSKY_IDENTIFIER` / `ROCKSKY_PASSWORD`. ## Options | Flag | Default | Description | | ---------------------- | ----------- | ----------------- | | `-p, --port ` | `6600` | Port to listen on | | `-b, --bind
` | `127.0.0.1` | Address to bind | Port and bind default to the `[mpd]` section of `~/.rocksky/settings.toml`; the flags override those values. If the chosen port is busy, the server automatically falls back to the next free one. ## Examples ```bash theme={null} # Start on the default MPD port (6600), localhost only rocksky mpd # Listen on all interfaces on a custom port rocksky mpd --bind 0.0.0.0 --port 6601 ``` Point a client at the server and browse your library: ```bash theme={null} # Recent tracks with mpc mpc -h 127.0.0.1 -p 6600 status # Or launch a full TUI client ncmpcpp -h 127.0.0.1 -p 6600 ``` ## Configuration Persist your preferred port and bind address in `~/.rocksky/settings.toml`: ```toml theme={null} [mpd] port = 6600 bind = "127.0.0.1" ``` Prefer a built-in interface? Run `rocksky` with no arguments to launch the interactive terminal UI, which browses your scrobbles and streams your music without a separate MPD client. # nowplaying Source: https://docs.rocksky.app/cli/nowplaying Show the currently playing track for yourself or any user. Print the track a user is currently listening to. Omit the DID to query your own account. ## Usage ```bash theme={null} rocksky nowplaying [did] ``` ## Parameters | Name | Required | Description | | ------- | -------- | ------------------------------------------------------------------ | | `[did]` | No | DID of the user to inspect. Defaults to the authenticated session. | ## Example ```bash theme={null} rocksky nowplaying did:plc:7vdlgi2bflelz7mmuxoqjfcr ``` # Overview Source: https://docs.rocksky.app/cli/overview Scrobble tracks, view stats, and manage your listening history from the terminal. `rocksky` is a command-line interface for the Rocksky API. Use it to scrobble plays, inspect listening stats, manage API keys, and run the MCP / local scrobble servers. ## Install ```bash Global theme={null} npm install -g @rocksky/cli ``` ```bash One-off theme={null} npx @rocksky/cli --help ``` ## Common workflow ```bash theme={null} rocksky login alice.bsky.social # one-time auth rocksky whoami # confirm session rocksky nowplaying # what's playing right now rocksky scrobbles --limit 20 # your recent history rocksky stats # listening stats ``` ## Commands | Command | Description | | ------------------------------------- | ---------------------------------------------------- | | [`login`](/cli/login) | Authenticate with your Bluesky handle | | [`whoami`](/cli/whoami) | Show the currently authenticated user | | [`nowplaying`](/cli/nowplaying) | Show the currently playing track | | [`scrobbles`](/cli/scrobbles) | List recent scrobbles | | [`scrobble`](/cli/scrobble) | Record a track to your profile | | [`search`](/cli/search) | Search tracks, albums, and accounts | | [`stats`](/cli/stats) | Listening stats for a user | | [`artists`](/cli/artists) | Top artists for a user | | [`albums`](/cli/albums) | Top albums for a user | | [`tracks`](/cli/tracks) | Top tracks for a user | | [`create apikey`](/cli/create-apikey) | Generate a new API key | | [`import`](/cli/import) | Import history from a Spotify or Last.fm export | | [`sync`](/cli/sync) | Sync local data from the AT Protocol | | [`mcp`](/cli/mcp) | Start the MCP server for Claude and other LLMs | | [`scrobble-api`](/cli/scrobble-api) | Run a local Last.fm / ListenBrainz-compatible server | | [`mpd`](/cli/mpd) | Serve your library over the MPD protocol | | [`help`](/cli/help) | Show help for any command | # scrobble Source: https://docs.rocksky.app/cli/scrobble Record a play to your Rocksky profile. Submit a one-off scrobble. Useful for testing integrations or recording a play from a source the CLI doesn't watch automatically. ## Usage ```bash theme={null} rocksky scrobble [options] ``` ## Parameters | Name | Required | Description | | ---------- | -------- | ------------------------------------- | | `` | Yes | Track title (quote multi-word values) | | `` | Yes | Artist name (quote multi-word values) | ## Example ```bash theme={null} rocksky scrobble "Karma Police" "Radiohead" ``` Run `rocksky help scrobble` to see the optional metadata flags (album, duration, timestamp, MBID, ISRC). # scrobble-api Source: https://docs.rocksky.app/cli/scrobble-api Run a local Last.fm / ListenBrainz / Web Scrobbler-compatible server. Start a local API server that accepts scrobbles from Last.fm, ListenBrainz, and Web Scrobbler clients, and forwards them to Rocksky. Useful for testing or running on a home server. ## Usage ```bash theme={null} rocksky scrobble-api [options] ``` ## Required environment variables ```bash theme={null} ROCKSKY_API_KEY=... ROCKSKY_SHARED_SECRET=... ROCKSKY_SESSION_KEY=... ROCKSKY_WEBSCROBBLER_KEY=... ROCKSKY_IDENTIFIER=alice.bsky.social ROCKSKY_PASSWORD=... ``` ## Endpoint summary | Client family | Base URL | | ----------------- | --------------------------------------------------------------- | | ListenBrainz | `http://localhost:8778` | | Last.fm (API 2.0) | `http://localhost:8778/2.0` | | Web Scrobbler | `http://localhost:8778/webscrobbler/` | See [Local scrobble API](/integrations/scrobble-api-local) for the end-to-end setup, including how to obtain each credential. # scrobbles Source: https://docs.rocksky.app/cli/scrobbles List recent scrobbles for yourself or another user. Show the most recent scrobbles for a user. ## Usage ```bash theme={null} rocksky scrobbles [options] [did] ``` ## Parameters | Name | Required | Description | | ------- | -------- | ------------------------------------------------------ | | `[did]` | No | DID to inspect. Defaults to the authenticated session. | ## Example ```bash theme={null} rocksky scrobbles did:plc:7vdlgi2bflelz7mmuxoqjfcr ``` Run `rocksky help scrobbles` to see the available flags (limit, offset, date ranges). # search Source: https://docs.rocksky.app/cli/search Search Rocksky for tracks, albums, and accounts. Free-text search across the Rocksky catalog. ## Usage ```bash theme={null} rocksky search [options] ``` ## Parameters | Name | Required | Description | | --------- | -------- | ------------------------------------------------ | | `` | Yes | Search string (artist, album, track, or account) | ## Example ```bash theme={null} rocksky search "linkin park" ``` # stats Source: https://docs.rocksky.app/cli/stats Show listening statistics for a user. Print aggregated listening stats for a user (scrobble totals, unique artists, unique tracks, etc.). ## Usage ```bash theme={null} rocksky stats [options] [did] ``` ## Parameters | Name | Required | Description | | ------- | -------- | ------------------------------------------------------ | | `[did]` | No | DID to inspect. Defaults to the authenticated session. | ## Example ```bash theme={null} rocksky stats did:plc:7vdlgi2bflelz7mmuxoqjfcr ``` # sync Source: https://docs.rocksky.app/cli/sync Sync local Rocksky data from the AT Protocol. Pull your scrobbles and metadata from the AT Protocol into local state. Useful when running the [local scrobble API](/cli/scrobble-api) or after restoring from a backup. ## Usage ```bash theme={null} rocksky sync ``` Run `rocksky help sync` for the optional flags. # tracks Source: https://docs.rocksky.app/cli/tracks Show top tracks for a user. List a user's top tracks, ranked by play count. ## Usage ```bash theme={null} rocksky tracks [options] [did] ``` ## Parameters | Name | Required | Description | | ------- | -------- | ------------------------------------------------------ | | `[did]` | No | DID to inspect. Defaults to the authenticated session. | ## Example ```bash theme={null} rocksky tracks did:plc:7vdlgi2bflelz7mmuxoqjfcr ``` # whoami Source: https://docs.rocksky.app/cli/whoami Show the currently authenticated user. Print the identity attached to the current CLI session — useful as a smoke test after [`rocksky login`](/cli/login). ## Usage ```bash theme={null} rocksky whoami ``` ## Example ```bash theme={null} $ rocksky whoami alice.bsky.social (did:plc:7vdlgi2bflelz7mmuxoqjfcr) ``` # FAQ Source: https://docs.rocksky.app/faq Common questions about Rocksky, scrobbling, self-hosting, and privacy. ## General Rocksky is a decentralized, open-source alternative to Last.fm built on the AT Protocol (the same protocol that powers Bluesky). It automatically tracks ("scrobbles") your music listening from many sources and publishes it to your own decentralized identity, giving you full ownership of your data. * You **own your data** on the AT Protocol — you can move it or self-host * Real-time **Stories** feed showing what your friends are playing * Strong compatibility with existing scrobblers * Fully open source and self-hostable * No corporate control or data lock-in Yes for the hosted version at [rocksky.app](https://rocksky.app). Any AT Protocol identity works, including one from a self-hosted PDS. No. Rocksky is an independent project built on top of the AT Protocol. It is not affiliated with or endorsed by Bluesky. ## Privacy & data Your scrobbles are stored on an **AT Protocol Personal Data Server (PDS)**. You control your data and can export or migrate it any time. Yes. You can delete individual scrobbles or your entire history at any time. ## Scrobbling & integrations Scrobbling is the act of automatically recording the songs you play — artist, track, album, timestamp, duration, etc. — and publishing them to your profile. * **Spotify** * **[Jellyfin](/integrations/jellyfin)** * **[Navidrome](/integrations/navidrome)** * **[Pano Scrobbler](/integrations/pano-scrobbler)** (Android, Windows, Linux) * **WebScrobbler** (Chrome / Firefox) * **[Kodi](/integrations/kodi)** * Any player that supports the **Last.fm** or **ListenBrainz** API Yes — especially with Spotify and supported native clients. ## Self-hosting Absolutely. Rocksky is designed to be fully self-hostable. It requires Docker, some technical knowledge, and ongoing maintenance. Detailed guides and Docker Compose files are provided. A simpler one-click setup is planned. Yes. ## Social & features A real-time decentralized feed showing what your Bluesky friends (and other Rocksky users) are listening to right now. Yes — likes and a shoutbox-style commenting system are supported. On the roadmap (high priority). ## Troubleshooting 1. Check that the scrobbler is pointing to the correct Rocksky endpoint 2. Verify your API key (if using manual setup) 3. Make sure your player is sending the required metadata The hosted version is still in active development; performance is improving quickly. Self-hosting close to you usually feels faster. ## Development Yes. Rocksky is open source and lives on Tangled at [tangled.org/@rocksky.app/rocksky](https://tangled.org/@rocksky.app/rocksky). You can explore the source, self-host, build integrations, and contribute. * Join the [Discord](https://discord.gg/EVcBy2fVa3) * Open issues or PRs on Tangled * Help improve documentation * Build integrations *** **Still have questions?** * Join the [Discord community](https://discord.gg/EVcBy2fVa3) * Open an issue on [Tangled](https://tangled.org/@rocksky.app/rocksky/issues/new) * Send us a message on [Bluesky](https://bsky.app/profile/did:plc:vegqomyce4ssoqs7zwqvgqty) # Introduction Source: https://docs.rocksky.app/index Rocksky is a decentralized, open-source music scrobbling network built on the AT Protocol. **Rocksky** is a decentralized, open-source alternative to Last.fm built on the [AT Protocol](https://atproto.com) — the same protocol that powers [Bluesky](https://bsky.app). It tracks ("scrobbles") the songs you play across your music apps and publishes them to your own decentralized identity, so your listening history belongs to you, not a single company. The hosted version is the fastest way to get started. Sign in with Bluesky and connect a source. A 5-minute walkthrough: sign in, connect a source, start scrobbling. ## Why Rocksky? * **You own your data.** Scrobbles live on your AT Protocol PDS, not on a vendor-controlled server. Export, migrate, or self-host whenever you want. * **Works with what you already use.** Spotify, Jellyfin, Navidrome, Kodi, Pano Scrobbler, WebScrobbler, plus anything that speaks the Last.fm or ListenBrainz protocol. * **Real-time and social.** A live **Stories** feed shows what people you follow are playing right now. Likes and shoutbox comments are first-class. * **Open source.** [Self-host the whole stack](https://tangled.org/@rocksky.app/rocksky), build integrations, or contribute on Tangled. ## What is "scrobbling"? Scrobbling means automatically recording the track you're listening to — artist, title, album, timestamp, duration — and publishing it to your profile. Rocksky scrobbles from your music apps in real time and makes the history queryable, shareable, and portable. ## How it works Use any AT Protocol identity. No separate Rocksky account to manage. Spotify connects in one click. Self-hosted players (Jellyfin, Navidrome, Kodi) point their ListenBrainz/Last.fm scrobbler at the Rocksky endpoint. Your plays show up on your profile and in the Stories feed within seconds. Browse charts, find new neighbours, follow friends, react to shouts, or pull your data through the [SDKs](/sdks/overview) and [API](/api-reference/introduction). ## Who is it for? Keep your listening history on infrastructure you control. A native scrobbling layer for the open social web. Plays nicely with Jellyfin, Navidrome, and your own PDS. Typed [SDKs in 10 languages](/sdks/overview), a Last.fm/ListenBrainz-compatible scrobble API, and an [MCP server](/cli/mcp) for LLMs. ## Next steps Scrobble from your Jellyfin library. Scrobble from Navidrome. Point a Last.fm-compatible scrobbler at Rocksky. Swap the API endpoint, reuse your existing clients. # Claude Desktop (MCP) Source: https://docs.rocksky.app/integrations/claude-desktop Query your scrobble history from Claude Desktop using the Rocksky MCP server. The Rocksky CLI ships a [Model Context Protocol](https://modelcontextprotocol.io) server. Plug it into Claude Desktop and Claude can read your scrobble history, top artists, now playing, and search the Rocksky catalog. ## Prerequisites * [Node.js](https://nodejs.org/) and `npm` * [Claude Desktop](https://claude.ai/download) * A [Rocksky](https://rocksky.app) account ## Install the CLI ```bash theme={null} npm install -g @rocksky/cli ``` ## Add the MCP server to Claude Desktop Open Claude Desktop's settings, find the **MCP** section, and add this entry to your config: ```json theme={null} { "mcpServers": { "rocksky": { "command": "rocksky", "args": ["mcp"] } } } ``` Restart Claude Desktop to load the server. ## What you can ask * "What were my top 10 tracks last week?" * "What am I listening to right now?" * "Show me the recent scrobbles for @alice.bsky.social" * "Search Rocksky for albums by Radiohead" * "Create a new API key called `my-bot`" Claude Desktop listing the user's recently played scrobbles via the Rocksky MCP server ## Troubleshooting * Confirm the CLI is on your PATH: `rocksky --help` * Make sure you've logged in once: `rocksky login ` * Restart Claude Desktop after editing the MCP config * Check Claude Desktop's MCP logs if a tool call fails See the [`rocksky mcp`](/cli/mcp) command reference for more. # Jellyfin Source: https://docs.rocksky.app/integrations/jellyfin Scrobble plays from your Jellyfin server to Rocksky. Rocksky speaks the ListenBrainz protocol, which means the community ListenBrainz plugin for Jellyfin works out of the box — you just need to point it at the Rocksky endpoint. ## Prerequisites * A running [Jellyfin](https://jellyfin.org) server * A [Rocksky](https://rocksky.app) account * An API key from the [API Applications](https://rocksky.app/apikeys) page ## Setup Install the community [ListenBrainz plugin](https://github.com/lyarenei/jellyfin-plugin-listenbrainz), then open its settings under **Dashboard → Plugins → ListenBrainz**. Replace the default endpoint: ``` https://api.listenbrainz.org ``` with the Rocksky endpoint: ``` https://audioscrobbler.rocksky.app ``` Paste your Rocksky API key into the **User token** field and save. Jellyfin ListenBrainz plugin configured with the Rocksky audioscrobbler URL Play a track in Jellyfin and refresh your Rocksky profile — the scrobble should appear in real time. Jellyfin sends a scrobble after you've played enough of a track. If a play doesn't register, scrub past \~30 seconds and let it keep playing. # Kodi Source: https://docs.rocksky.app/integrations/kodi Scrobble plays from Kodi to Rocksky using the official ListenBrainz add-on. Kodi ships a ListenBrainz Scrobbler add-on in the official repository. With two settings, you can point it at Rocksky. ## Prerequisites * [Kodi](https://kodi.tv/) media center * A [Rocksky](https://rocksky.app) account * An API key from the [API Applications](https://rocksky.app/apikeys) page ## Setup In Kodi, go to **Add-ons → Install from repository → Kodi Add-on repository → Services → ListenBrainz Scrobbler** and install it. Open the add-on settings and change the ListenBrainz API endpoint to: ``` https://audioscrobbler.rocksky.app ``` Paste your Rocksky API key into the **Token** field. Kodi ListenBrainz Scrobbler settings configured with the Rocksky audioscrobbler URL Restart Kodi, play a track, and check your Rocksky profile to confirm the scrobble appears. # Navidrome Source: https://docs.rocksky.app/integrations/navidrome Scrobble from a Navidrome server to Rocksky using the ListenBrainz-compatible API. Navidrome ships with native ListenBrainz scrobbling. Rocksky implements that same protocol, so the integration is a one-line config change. ## Prerequisites * A running [Navidrome](https://www.navidrome.org/) server * A [Rocksky](https://rocksky.app) account * An API key from the [API Applications](https://rocksky.app/apikeys) page ## Configure Navidrome Enable ListenBrainz scrobbling, then point Navidrome at the Rocksky endpoint. ```toml navidrome.toml theme={null} ListenBrainz.Enabled = true ListenBrainz.BaseURL = "https://audioscrobbler.rocksky.app/1/" ``` ```bash Environment theme={null} ND_LISTENBRAINZ_ENABLED=true ND_LISTENBRAINZ_BASEURL=https://audioscrobbler.rocksky.app/1/ ``` Restart Navidrome to pick up the changes. ## Add your API key 1. Open your Navidrome profile. 2. Enable **ListenBrainz** scrobbling. 3. Paste your Rocksky API key into the token field. ## Verify Play a track through Navidrome and check your [Rocksky profile](https://rocksky.app) — the scrobble should appear in your history. Navidrome only sends a scrobble once you've played enough of a track. # Rocksky as a personal Navidrome server Source: https://docs.rocksky.app/integrations/navidrome-server Stream your uploaded library through any Navidrome / Subsonic-compatible client, using Rocksky as the backend. Rocksky exposes a Subsonic / Navidrome-compatible API. Upload your music to your private Rocksky library and stream it from any Subsonic client — phone, desktop, car head unit — without running your own server. ## Step 1: Upload music Open **My Library** in Rocksky's sidebar: Rocksky sidebar with 'My Library (experimental)' link and the Scrobble Stats chart Drag and drop your audio files (MP3, FLAC, M4A, OGG, WAV, AIFF) into the upload page. Required tags: title, artist, album, album artist, duration, album art. Need to tag your files first? [MusicBrainz Picard](https://picard.musicbrainz.org/) auto-fills metadata from the MusicBrainz database. Rocksky Upload Music page with a track successfully uploaded Your uploaded music is private — only you can access and stream your files. ## Step 2: Get an API key Visit the [API Keys](https://rocksky.app/apikeys) page and generate a new key. You'll use it as the password in your Subsonic client. ## Step 3: Configure your client In any Subsonic / Navidrome-compatible app, add a new server with these details: | Field | Value | | ---------- | ---------------------------------------------- | | Server URL | `https://navidrome.rocksky.app` | | Username | Your ATProto handle (e.g. `alice.bsky.social`) | | Password | Your Rocksky API key | Cassette desktop app configured against navidrome.rocksky.app ## Compatible apps * **Symfonium** (Android) * **Substreamer** (Android, iOS) * **Cassette** (iOS) * **play:Sub** (iOS) * **Amperfy** (iOS, macOS) * **Tempo** (iOS, macOS) * …and many other Navidrome / Subsonic clients. ## Scrobbling Plays from any of these clients are scrobbled automatically — there's nothing extra to wire up. Rocksky currently stores uploads on Cloudflare R2. Support for user-connected S3-compatible providers (bring-your-own bucket) is on the roadmap. # Pano Scrobbler (Android) Source: https://docs.rocksky.app/integrations/pano-scrobbler Scrobble from any Android music player using Pano Scrobbler. [Pano Scrobbler](https://github.com/kawaiiDango/pano-scrobbler) watches playback on your Android device and forwards each play to a scrobbling service. Point it at Rocksky's ListenBrainz endpoint and every Android player on your phone starts scrobbling. ## Prerequisites * Android device with Pano Scrobbler installed * A [Rocksky](https://rocksky.app) account * An API key from the [API Applications](https://rocksky.app/apikeys) page ## Setup Launch the app on your Android device. Go to **Settings** and scroll to the **ListenBrainz** section. Enable it if it isn't already. Tap **Custom server URL** and enter: ``` https://audioscrobbler.rocksky.app ``` Paste your Rocksky API key into the **Authentication token** field, then save. Pano Scrobbler Custom ListenBrainz settings with the Rocksky API URL filled in Once it's running, Pano Scrobbler watches every playing app on your device: Pano Scrobbler home screen listing recently played tracks ## Verify Start playing a track. Visit [rocksky.app](https://rocksky.app) — your recent scrobbles should appear in real time. Pano Scrobbler waits until enough of a track has played before submitting it. # Local scrobble API Source: https://docs.rocksky.app/integrations/scrobble-api-local Run a local ListenBrainz / Last.fm / Web Scrobbler-compatible server backed by Rocksky. The Rocksky CLI can boot a local scrobble server that speaks the ListenBrainz, Last.fm, and Web Scrobbler protocols. Point any compatible client at `localhost` and have your plays forwarded to Rocksky — handy for offline-tolerant setups, testing integrations, or running on a home server. `npx @rocksky/cli scrobble-api` running in a terminal, syncing scrobbles and connecting to JetStream ## Prerequisites * [Node.js](https://nodejs.org/) * An AT Protocol (Bluesky) account ## Configure credentials Set these environment variables once so the server can authenticate after a restart: ```bash theme={null} export ROCKSKY_API_KEY=... export ROCKSKY_SHARED_SECRET=... export ROCKSKY_SESSION_KEY=... export ROCKSKY_WEBSCROBBLER_KEY=... export ROCKSKY_IDENTIFIER=alice.bsky.social # your handle or DID export ROCKSKY_PASSWORD=... # app password from https://bsky.app/settings/app-passwords ``` ## Start the server ```bash theme={null} npx @rocksky/cli scrobble-api ``` The server listens on `http://localhost:8778`. First startup can take a while: it syncs your existing scrobble history locally before accepting writes. ## Point your clients at it Base URL: `http://localhost:8778`. Use your `ROCKSKY_API_KEY` as the authentication token. Base URL: `http://localhost:8778/2.0`. Use your `ROCKSKY_API_KEY` and `ROCKSKY_SESSION_KEY`. Endpoint: `http://localhost:8778/webscrobbler/` See the [`rocksky scrobble-api`](/cli/scrobble-api) command reference for the full surface. # Web Scrobbler Source: https://docs.rocksky.app/integrations/web-scrobbler Scrobble plays from YouTube, SoundCloud, Spotify Web, and 100+ other sites with the Web Scrobbler browser extension. [Web Scrobbler](https://github.com/web-scrobbler/web-scrobbler) is a free browser extension that detects what you're playing on YouTube, SoundCloud, Spotify Web, Bandcamp, Apple Music Web, and over 100 other sites. Point its custom webhook at Rocksky and every detected play lands in your timeline. ## Prerequisites * A [Rocksky](https://rocksky.app) account * A supported browser (Chrome, Firefox, Edge, or any Chromium-based browser) ## Setup Install the extension from the [Web Scrobbler repository](https://github.com/web-scrobbler/web-scrobbler) or your browser's add-on store. Pin it to the toolbar so you can confirm detections. Sign in to [rocksky.app](https://rocksky.app), open the avatar menu in the top-right, and select **Web Scrobbler**. A modal pops up with a personal webhook URL of the form: ``` https://webscrobbler.rocksky.app/ ``` Click the copy icon next to the URL. Your webhook UUID is created automatically the first time you open the Web Scrobbler modal. Keep it private — anyone with the URL can post scrobbles to your account. Open the Web Scrobbler extension settings and go to **Accounts → Webhook → API URL**. Paste the URL you copied and save. Web Scrobbler extension settings — Accounts › Webhook › API URL field showing the Rocksky webhook URL Play a song on any supported site (YouTube is the easiest test) and let it run past the scrobble threshold (usually \~30 seconds or 50% of the track, whichever comes first). Refresh your Rocksky profile — the play should appear. ## What gets sent Web Scrobbler posts a JSON payload to the webhook for two event types: * **Now playing** — when a track starts. Surfaces as your current "Now playing" status on Rocksky. * **Scrobble** — when the track meets Web Scrobbler's threshold. Stored as a permanent scrobble. Track metadata is best-effort — Web Scrobbler reads what each site exposes, so the title/artist/album fidelity depends on the source. Rocksky normalises the incoming text and tries to match it against its catalog. ## Rotating the webhook If your webhook URL leaks, reach out on [Discord](https://discord.gg/EVcBy2fVa3) to have the UUID rotated. After rotation, paste the new URL into Web Scrobbler's API URL field — the old one will start rejecting submissions. Web Scrobbler detection runs in the page context of each site. Some sites change their DOM and break detection temporarily — check [the Web Scrobbler issue tracker](https://github.com/web-scrobbler/web-scrobbler/issues) if a specific site stops scrobbling. # Migrating from Last.fm Source: https://docs.rocksky.app/migrations/from-lastfm Repoint any Last.fm Audioscrobbler-compatible client at Rocksky. Rocksky implements the Last.fm Audioscrobbler API (both the modern 2.0 protocol and the legacy submission protocol). Migrating an existing client usually means changing one URL and swapping credentials. ## Get your credentials 1. Sign in to [rocksky.app](https://rocksky.app). 2. Go to your [API Applications](https://rocksky.app/apikeys). 3. Create an application to get an **API key** and **shared secret**. 4. Generate a **session key** with [`rocksky login`](/cli/login). ## Modern Last.fm clients (API 2.0) Replace the base URL: | Last.fm | Rocksky | | ----------------------------------- | ---------------------------------------- | | `https://ws.audioscrobbler.com/2.0` | `https://audioscrobbler.rocksky.app/2.0` | Authentication still uses `api_key` and `sk` (session key) parameters with MD5-signed requests: 1. Sort all request parameters by key alphabetically. 2. Concatenate `key + value` pairs into one string. 3. Append your shared secret. 4. MD5-hash the result; send it as `api_sig`. ## Legacy submission protocol For older clients (e.g. Deadbeef), use the legacy endpoint: ``` https://audioscrobbler.rocksky.app ``` * **Username**: your API key * **Password**: your shared secret ## Metadata normalization Rocksky normalizes incoming track metadata before storing it. If the track can't be matched against a known song, the scrobble is skipped rather than stored as an orphan record. ## Why migrate? * Your scrobbles live on infrastructure you can self-host * Real-time **Stories** feed for what your network is playing * AT Protocol identity — portable, exportable, no lock-in * Compatible with existing tools you already trust Need help? Open a thread in the [Discord](https://discord.gg/EVcBy2fVa3). # Migrating from ListenBrainz Source: https://docs.rocksky.app/migrations/from-listenbrainz Switch any ListenBrainz client over to Rocksky in two steps. Rocksky exposes a ListenBrainz-compatible API for the core scrobbling endpoints. Most ListenBrainz clients can be moved over by changing the base URL and the API token. ## Prerequisites * A [Rocksky](https://rocksky.app) account * An API key from the [API Applications](https://rocksky.app/apikeys) page * A ListenBrainz-compatible scrobbler ## Step 1: Change the base URL | ListenBrainz | Rocksky | | ------------------------------ | ------------------------------------ | | `https://api.listenbrainz.org` | `https://audioscrobbler.rocksky.app` | ## Step 2: Use your Rocksky API key Update the `Authorization` header your client sends: ``` Authorization: Token ``` ## Step 3: Test it Submit a sample scrobble to `/submit-listens` with valid metadata, then check your [Rocksky profile](https://rocksky.app) to confirm it landed. ## Supported endpoints The Rocksky audioscrobbler implements the core ListenBrainz endpoints, including: * `POST /1/submit-listens` * `GET /1/validate-token` ## Metadata normalization Rocksky tries to normalize incoming track metadata against its catalog. If a match cannot be found, the scrobble may be skipped instead of stored. ## Per-client setup If you use one of these clients, the per-client guide already covers the URL swap: * [Jellyfin](/integrations/jellyfin) * [Navidrome](/integrations/navidrome) * [Pano Scrobbler (Android)](/integrations/pano-scrobbler) * [Kodi](/integrations/kodi) # Mirror from Last.fm Source: https://docs.rocksky.app/mirroring/lastfm Pull plays from a Last.fm account into Rocksky on a 30-second poll. Rocksky polls Last.fm's `user.getRecentTracks` endpoint every 30 seconds and mirrors any new scrobble into your Rocksky timeline. Your existing Last.fm setup keeps working unchanged. ## Prerequisites * A [Rocksky](https://rocksky.app) account * A Last.fm account that's actively receiving scrobbles * A Last.fm API key (free, instant — see below) ## Setup Go to [last.fm/api/account/create](https://www.last.fm/api/account/create) and fill in the form. The "callback URL" and "application homepage" fields can be anything — they aren't used for read-only access. Copy the 32-character **API key**. You can ignore the shared secret — `user.getRecentTracks` is a read-only endpoint and doesn't need signed requests. Sign in to Rocksky and open [rocksky.app/mirrors](https://rocksky.app/mirrors). Select the **Last.fm** tab. * **Username** — your Last.fm handle (the one in your profile URL, e.g. `last.fm/user/`). * **Last.fm API key** — the 32-character key from step 1. Click **Save credentials**. The API key is encrypted with XSalsa20-Poly1305 before being written to the database. Flip the **Mirror enabled** toggle on. Within \~30 seconds the worker picks up the change and starts polling. ## What gets mirrored * Completed scrobbles from `user.getRecentTracks`, newest first. * Currently-playing tracks are skipped — Rocksky only mirrors plays that have a `date` timestamp. * On first enable, a 24-hour backfill window is seeded so recent plays land too, not just future ones. ## Dedup behaviour Each candidate play is checked against your existing scrobbles within a ±120-second window. If you also scrobble to Rocksky directly (from Jellyfin, Navidrome, the browser extension, etc.), you won't get duplicates — whichever arrives first wins. ## Rotating or removing the key * **Rotate** — type a new API key into the field and save. The previous encrypted value is overwritten. * **Remove** — clear the API key and save. The mirror source becomes unauthenticated and the worker exits. # Mirror from ListenBrainz Source: https://docs.rocksky.app/mirroring/listenbrainz Pull listens from a ListenBrainz account into Rocksky on a 30-second poll. Rocksky polls ListenBrainz's `/1/user/{name}/listens` endpoint every 30 seconds and mirrors new listens into your Rocksky timeline. ## Prerequisites * A [Rocksky](https://rocksky.app) account * A ListenBrainz account that's actively receiving listens * A ListenBrainz user token ## Setup While signed in to ListenBrainz, open [listenbrainz.org/settings](https://listenbrainz.org/settings/) and copy your **user token**. Sign in to Rocksky and open [rocksky.app/mirrors](https://rocksky.app/mirrors). Select the **ListenBrainz** tab. * **Username** — your ListenBrainz handle. * **ListenBrainz user token** — the token from step 1. Click **Save credentials**. The token is encrypted with XSalsa20-Poly1305 before being written to the database. Flip the **Mirror enabled** toggle on. The worker picks up the new source within \~30 seconds and starts polling. ## What gets mirrored * Listens returned from `/1/user/{name}/listens`, scoped server-side with `min_ts` so each poll only pulls what's new since the last watermark. * On first enable, a 24-hour backfill window is seeded. ## Dedup behaviour Each listen is checked against your existing scrobbles within a ±120-second window. If you already scrobble to Rocksky from a client that also writes to ListenBrainz, you won't get duplicates. ## Rotating or removing the token * **Rotate** — paste a new token and save. * **Remove** — clear the token and save; the worker exits. ListenBrainz's listens endpoint is publicly readable for non-private accounts, but Rocksky still requires a token so polling stays under the authenticated rate limit and mirrors keep up. # Scrobble mirroring Source: https://docs.rocksky.app/mirroring/overview Keep using Last.fm, ListenBrainz, or Teal.fm — and have those plays mirror into Rocksky automatically. Mirroring is the lighter-touch alternative to fully [migrating](/migrations/from-lastfm) off another scrobbler. You keep your existing setup exactly as it is — Rocksky just pulls new plays from your account on the other service and re-publishes them as scrobbles on your account. ## How it differs from migrating | | Migrating | Mirroring | | ------------------------------- | ------------------- | -------------------------------------------- | | Your clients send scrobbles to | Rocksky | Last.fm / ListenBrainz / Teal.fm (unchanged) | | Plays land in the other service | No | Yes | | Plays land in Rocksky | Yes | Yes | | Setup | Repoint each client | One toggle in your Rocksky settings | Pick mirroring when you want Rocksky alongside an existing scrobbler. Pick migrating when you're ready to point your clients directly at Rocksky and stop double-writing. ## Supported sources Polls `user.getRecentTracks` every 30 seconds. Polls `/user/{name}/listens` every 30 seconds. Real-time via AT Protocol Jetstream — no polling, no credentials. ## How it works 1. You enter your username (and an API key, for Last.fm and ListenBrainz) on the [Mirrors page](https://rocksky.app/mirrors) and flip the **Mirror enabled** toggle. 2. Credentials are encrypted at rest with XSalsa20-Poly1305 before being written to the database. 3. A background worker picks up the new source within a few seconds and starts polling (or, for Teal.fm, subscribing to Jetstream). 4. Each play newer than the last-seen watermark runs through a ±120-second dedup check against your existing scrobbles. Anything genuinely new is published as `app.rocksky.scrobble.createScrobble`. 5. On first enable, the watermark is seeded 24 hours in the past — so you get a one-day backfill rather than only future plays. ## Disabling Flip the toggle off and the worker stops polling within a poll cycle (≤30s). Your stored credentials stay encrypted in the database so you can re-enable later without re-entering them. Clear them by saving an empty API key. ## FAQ **Will I get duplicate scrobbles if my client also writes directly to Rocksky?** No. The dedup check looks ±120 seconds around each candidate play and skips anything already in your scrobble history from another source. **What happens if my API key expires or is revoked?** The worker logs the auth failure and stops polling that source. Save a new key on the Mirrors page to resume. **Can I mirror more than one source at once?** Yes. Each provider runs independently — enable any combination. # Mirror from Teal.fm Source: https://docs.rocksky.app/mirroring/tealfm Subscribe to Teal.fm play events in real time over AT Protocol Jetstream. Teal.fm publishes plays as AT Protocol records, so Rocksky mirrors them through a single [Jetstream](https://atproto.com/blog/jetstream) subscription to `fm.teal.alpha.feed.play`. No polling, no API keys — your ATProto DID is the only identifier needed. ## Prerequisites * A [Rocksky](https://rocksky.app) account (signed in with the same ATProto identity you use on Teal.fm) * An active Teal.fm account publishing play records ## Setup Sign in to Rocksky and open [rocksky.app/mirrors](https://rocksky.app/mirrors). Select the **Teal.fm** tab. Flip the **Mirror enabled** toggle on. There are no credentials to enter — Rocksky filters the Jetstream firehose for play records whose `did` matches yours. ## How it works * One process-wide WebSocket connection subscribes to `fm.teal.alpha.feed.play` on Jetstream. * Enabling mirroring adds your DID to an in-memory set; the next play record from that DID gets normalised and re-published as a Rocksky scrobble. * Disabling removes your DID from the set — within seconds, no further events are processed. Because there's no polling, plays show up within a couple of seconds of Teal.fm broadcasting them to the relay. ## Dedup behaviour Each event runs through the same ±120-second dedup check as the polled sources. If you also scrobble to Rocksky from another client at the same time, only one record lands. Teal.fm mirroring relies on the AT Protocol relay being reachable. If the Jetstream connection drops, Rocksky reconnects automatically — but any plays broadcast during the gap may be missed. Use Last.fm or ListenBrainz mirroring alongside it if you need polled redundancy. # Quick start Source: https://docs.rocksky.app/quickstart Get up and running with Rocksky in under 5 minutes. Rocksky is a decentralized Last.fm alternative. It lets you automatically track your music listening ("scrobble") and publish it to your Bluesky / AT Protocol identity. Sign in with Bluesky and start scrobbling. ## Option 1: Use the hosted version No server required — sign in and start scrobbling. Go to [rocksky.app](https://rocksky.app) and click **Sign in with Bluesky**. Log in with your Bluesky account (or create one). Pick one or more sources below. | Source | How to connect | Difficulty | | ----------------- | ---------------------------------------------------- | ---------- | | **Spotify** | Connect from Settings | Easy | | **Jellyfin** | [Add Rocksky as a scrobbler](/integrations/jellyfin) | Easy | | **Navidrome** | [Configure Navidrome](/integrations/navidrome) | Easy | | **Android** | [Pano Scrobbler](/integrations/pano-scrobbler) | Easy | | **Browser** | Install the WebScrobbler extension | Easy | | **Kodi** | [Install the Rocksky add-on](/integrations/kodi) | Medium | | **Other players** | Any Last.fm or ListenBrainz-compatible scrobbler | Easy | Your plays appear on your profile and in the **Stories** feed within seconds. ## Option 2: Self-host Rocksky For full control and privacy. **Prerequisites** * Docker + Docker Compose * Node.js or Bun (for development) ```bash theme={null} git clone https://tangled.org/@rocksky.app/rocksky cd rocksky # follow the self-hosting guide in the repo README ``` A one-command setup is on the roadmap. In the meantime, the [source repo](https://tangled.org/@rocksky.app/rocksky) is the canonical starting point. ## What next? Repoint any Last.fm-compatible scrobbler at Rocksky. `rocksky` is a CLI for scrobbling, stats, and managing API keys. Plug the MCP server into Claude Desktop. SDKs for TypeScript, Python, Rust, Go, Ruby, Kotlin, Elixir, Erlang, Clojure, and Gleam. # Clojure Source: https://docs.rocksky.app/sdks/clojure Clojure SDK for Rocksky — native core over the JVM Panama FFM API. `app.rocksky/sdk` binds the shared Rocksky Rust core via the JVM **Panama FFM** API (`rocksky.core`, JDK 22+): AppView reads, AT Protocol PDS writes, and the identity hashes — the same engine behind every Rocksky SDK. ## Install deps.edn: ```clojure theme={null} app.rocksky/sdk {:mvn/version "0.6.0-SNAPSHOT"} ``` The jar is native-free; the library is fetched from the GitHub release on first use. Run with `--enable-native-access=ALL-UNNAMED` (the `:native` alias adds it). ## Quickstart ```clojure theme={null} (require '[rocksky.core :as core]) ;; Reads — unauthenticated. An optional trailing base overrides the AppView URL. (core/global-stats) (doseq [t (core/top-tracks 10 0)] (println (get t "artist") "—" (get t "title"))) ;; Writes — log in with an app password. (def agent (core/login "session.json" "alice.bsky.social" "app-password")) (core/scrobble agent {"title" "Chaser" "artist" "Calibro 35" "album" "Jazzploitation" "albumArtist" "Calibro 35" "durationMs" 182320}) (core/agent-close agent) ``` ## API Reads/writes return plain maps; write verbs throw `ex-info` on failure. **Reads — `rocksky.core`**: named reads `(profile actor)`, `(scrobbles actor limit)`, `(top-tracks limit offset)`, `(global-stats)` — each accepts a trailing `base`. **Universal escape hatch**: `(query nsid params)` reaches the whole `app.rocksky.*` read catalog and returns parsed data — e.g. `"app.rocksky.album.getAlbums"`, `"app.rocksky.album.getAlbumTracks"`, `"app.rocksky.graph.getFollows"`, `"app.rocksky.actor.getActorLovedSongs"`, `"app.rocksky.stats.getStats"`, `"app.rocksky.charts.getScrobblesChart"`. `(query nsid params base token)` sends `token` as `Authorization: Bearer` for auth-gated queries. **Typed date-window charts**: `(top-tracks-interval limit offset interval)` and `(top-artists-interval limit offset interval)` — `interval` is `:all` or `[:days n]` / `[:weeks n]` / `[:months n]` / `[:years n]` / `[:range start end]`, e.g. `(top-tracks-interval 5 0 [:days 7])`. **Match**: `(match-song title artist)` resolves a bare title + artist into full canonical metadata. **Writes — `rocksky.core`**: `(login ... :dedup-path "./dedup")`, then `(scrobble agent track)` (full metadata) or `(scrobble-match agent params)` — a map with camelCase keys, required `:title`/`:artist`, optional `:album`, `:mbId`, `:isrc` (match anchors) and `:timestamp` (scrobbled-at Unix seconds) — match-then-write, `(like agent uri cid)`, `(follow agent did)`, `(shout ...)`, `(refresh-session agent)`, `(agent-close agent)`. With `:dedup-path` set, `(sync-repo agent)` (repo backfill) and `(hydrate-from-jetstream agent)` (live stream) run deduped against the on-disk store. **Identity hashes**: `(song-hash title artist album)`. # Elixir Source: https://docs.rocksky.app/sdks/elixir Elixir SDK for Rocksky — over the shared Rustler NIF. `rocksky_ex` wraps the shared Rocksky Rust core through a Rustler **NIF**: AppView reads, AT Protocol PDS writes, and the identity hashes — the same engine behind every Rocksky SDK. The same NIF powers the [Erlang](/sdks/erlang) and [Gleam](/sdks/gleam) SDKs. ## Install ```elixir theme={null} def deps do [{:rocksky_ex, "~> 0.6"}] end ``` `rocksky_ex` 0.6.0 pulls in `rocksky_erl` 0.3.0, whose loader fetches the native library from the GitHub release on first use. ## Quickstart ```elixir theme={null} # Reads — unauthenticated. The last arg overrides https://api.rocksky.app. {:ok, stats} = Rocksky.global_stats() IO.puts(stats["scrobbles"]) {:ok, top} = Rocksky.top_tracks(10, 0) for t <- top, do: IO.puts("#{t["artist"]} — #{t["title"]}") # Writes — log in with an app password. agent = Rocksky.login("session.json", "alice.bsky.social", "app-password") {:ok, out} = Rocksky.scrobble(agent, %{ "title" => "Chaser", "artist" => "Calibro 35", "album" => "Jazzploitation", "albumArtist" => "Calibro 35", "durationMs" => 182_320 }) IO.puts(out["scrobbleUri"]) ``` Using Erlang directly? The same NIF ships as `rocksky_erl` with a friendly `rocksky` module — see the [Erlang SDK](/sdks/erlang) page. ## API Reads/writes return `{:ok, value}` | `{:error, message}` with binary-keyed maps. **Reads — `Rocksky`**: named reads `profile`, `scrobbles`, `top_tracks`, `global_stats` (each takes a trailing `base`). The universal `get(nsid, params \\ %{}, base \\ "", token \\ "")` escape hatch reaches the *whole* `app.rocksky.*` read catalog by NSID (e.g. `Rocksky.get("app.rocksky.album.getAlbums", %{"limit" => 20})`, `"app.rocksky.album.getAlbumTracks"`, `"app.rocksky.graph.getFollows"`, `"app.rocksky.stats.getStats"`); pass a bearer `token` for auth-gated queries. Typed date-window charts: `top_tracks_interval(5, 0, {:days, 7})` / `top_artists_interval(5, 0, :all)` where the interval is `:all` | `{:days, n}` | `{:weeks, n}` | `{:months, n}` | `{:years, n}` | `{:range, start, end}`. `match_song(title, artist)` resolves a bare title + artist into full canonical metadata. **Writes — `Rocksky`**: `login(session, id, pw, appview \\ "", dedup_path \\ "")`, then `scrobble` (full metadata) or `scrobble_match(agent, input)` — `input` is a map with camelCase string keys, required `"title"`/`"artist"`, optional `"album"`, `"mbId"`, `"isrc"` (match anchors) and `"timestamp"` (scrobbled-at Unix seconds), e.g. `Rocksky.scrobble_match(agent, %{"title" => "Chaser", "artist" => "Calibro 35"})` — then `like`, `follow`, `shout`, `refresh_session`. Pass a `dedup_path` at login to enable dedup + realtime, then `sync_repo(agent)` / `hydrate_from_jetstream(agent)`. **Identity hashes**: `Rocksky.song_hash(title, artist, album)`. # Erlang Source: https://docs.rocksky.app/sdks/erlang Erlang SDK for Rocksky — over the shared Rustler NIF. `rocksky_erl` wraps the shared Rocksky Rust core through a Rustler **NIF**: AppView reads, AT Protocol PDS writes, and the identity hashes — the same engine behind every Rocksky SDK. The same NIF powers the [Elixir](/sdks/elixir) and [Gleam](/sdks/gleam) SDKs. The OTP application is `rocksky_erl`; the modules are `rocksky` (the friendly API) and `rocksky_nif` (the raw NIF). Its loader fetches the native library from the GitHub release on first use. ## Install ```erlang theme={null} %% rebar.config {deps, [{rocksky_erl, "0.3.0"}]}. ``` ## Quickstart ```erlang theme={null} %% Reads — unauthenticated. A trailing Base overrides https://api.rocksky.app. {ok, Stats} = rocksky:global_stats(), io:format("~p scrobbles~n", [maps:get(<<"scrobbles">>, Stats)]), %% Universal escape hatch — reaches the whole app.rocksky.* read catalog: {ok, Albums} = rocksky:get(<<"app.rocksky.album.getAlbums">>, #{<<"limit">> => 20}), {ok, Top} = rocksky:top_tracks_interval(5, 0, {days, 7}), {ok, Song} = rocksky:match_song(<<"Chaser">>, <<"Calibro 35">>), %% Writes — log in with an app password. Agent = rocksky:agent_login(<<"session.json">>, <<"alice.bsky.social">>, <<"app-pw">>), {ok, Out} = rocksky:agent_scrobble_match(Agent, #{<<"title">> => <<"Chaser">>, <<"artist">> => <<"Calibro 35">>}). ``` Reads/writes return `{ok, Value}` | `{error, Message}` with binary-keyed maps. ## API ### Reads Named reads: `profile`, `scrobbles`, `top_tracks`, `global_stats` (each takes a trailing `Base`). **Universal `get`** — the escape hatch reaches the *whole* `app.rocksky.*` read catalog by NSID: `rocksky:get(Nsid, Params)`, `rocksky:get(Nsid, Params, Base)`, and `rocksky:get(Nsid, Params, Base, Token)` (bearer token for auth-gated queries), each → `{ok, Data}`. ```erlang theme={null} {ok, Albums} = rocksky:get(<<"app.rocksky.album.getAlbums">>, #{<<"limit">> => 20}), {ok, Tracks} = rocksky:get(<<"app.rocksky.album.getAlbumTracks">>, #{<<"uri">> => Uri}), {ok, Follows} = rocksky:get(<<"app.rocksky.graph.getFollows">>, #{<<"actor">> => Actor}), {ok, S} = rocksky:get(<<"app.rocksky.stats.getStats">>, #{}). ``` **Typed date-window charts** — `rocksky:top_tracks_interval(5, 0, {days, 7})` and `rocksky:top_artists_interval(5, 0, all)`; the interval is `all` | `{days, N}` | `{weeks, N}` | `{months, N}` | `{years, N}` | `{range, Start, End}`. Plain `top_tracks` / `top_artists` are all-time shorthands. **Match** — `rocksky:match_song(Title, Artist)` resolves a bare title + artist into full canonical metadata (album, artwork, duration, MBID, ISRC, links). ### Writes `rocksky:agent_login(Session, Id, Pw)` (also `/5` with `AppView`, `DedupPath`), then `agent_scrobble` (full metadata), `agent_like`, `agent_follow`, `agent_shout`, `agent_refresh_session`. **Match-then-scrobble** — `rocksky:agent_scrobble_match(Agent, Input)` where `Input` is a map with camelCase binary keys: required `<<"title">>`/`<<"artist">>`, optional `<<"album">>`, `<<"mbId">>`, `<<"isrc">>` (match anchors) and `<<"timestamp">>` (scrobbled-at Unix seconds). Resolves canonical metadata and scrobbles in one call. (A flat `/7` form — `agent_scrobble_match(Agent, Title, Artist, Album, MbId, Isrc, Timestamp)`, empty strings / `0` omitted — backs the Gleam SDK.) **Dedup + realtime** — pass a `DedupPath` to `agent_login/5` to enable the local dedup store, then keep it warm with `rocksky:agent_sync_repo(Agent)` and `rocksky:agent_hydrate_from_jetstream(Agent)`. ### Identity hashes `rocksky:song_hash(Title, Artist, Album)` / `album_hash` / `artist_hash` — lowercase-hex SHA-256, byte-for-byte identical across every Rocksky SDK and the server. # Gleam Source: https://docs.rocksky.app/sdks/gleam Gleam SDK for Rocksky — over the shared Rustler NIF. `rocksky` exposes the shared Rocksky Rust core via typed externals over the `rocksky_erl` Rustler NIF (`rocksky/client` module): AppView reads, AT Protocol PDS writes, and the identity hashes. Targets Erlang. ## Install ```bash theme={null} gleam add rocksky # rocksky = ">= 1.6.0 and < 2.0.0" ``` `rocksky` 1.6.0 depends on `rocksky_erl` 0.3.0, whose loader fetches the native library from the GitHub release on first use. ## Quickstart ```gleam theme={null} import rocksky/client pub fn main() { // Reads — use the default AppView (https://api.rocksky.app). // Envelope calls return Dynamic ({ok, value} | {error, message}). echo client.global_stats() echo client.top_tracks(10, 0) // Writes — log in with an app password. let agent = client.login("session.json", "alice.bsky.social", "app-password") echo client.follow(agent, "did:plc:rlwgbwqdknilpxxep5gvzc3y") } ``` ## API **Reads**: named reads `profile(actor)`, `scrobbles(actor, limit, offset)`, `top_tracks(limit, offset)`, `global_stats()` — each returns `Dynamic`. Use the `*_at` variants (e.g. `global_stats_at(endpoint)`) to target a custom AppView. The universal `get(nsid, params_json)` escape hatch reaches the *whole* `app.rocksky.*` read catalog by NSID (e.g. `client.get("app.rocksky.album.getAlbums", "{\"limit\":20}")`, `"app.rocksky.album.getAlbumTracks"`, `"app.rocksky.graph.getFollows"`, `"app.rocksky.stats.getStats"`); `get_authed(nsid, params_json, token)` sends a bearer token for auth-gated queries and `get_at(nsid, params_json, endpoint)` targets a custom AppView. Typed date-window charts: `top_tracks_interval(5, 0, client.LastDays(7))` / `top_artists_interval` where `Interval` is `AllTime | LastDays(Int) | LastWeeks(Int) | LastMonths(Int) | LastYears(Int) | Range(String, String)`. `match_song(title, artist)` resolves a bare title + artist into full canonical metadata. **Writes**: `login(session_path, identifier, password)`, then `scrobble` (full metadata) or `scrobble_match(agent, ScrobbleMatch(title, artist, album, mb_id, isrc, timestamp))` (match-then-write) — `title`/`artist` are `String`, the rest `Option` (`album` override, `mb_id`/`isrc` match anchors, `timestamp` scrobbled-at Unix seconds), e.g. `scrobble_match(agent, ScrobbleMatch("Chaser", "Calibro 35", None, None, None, None))` — `like`, `follow`, `shout`, `refresh_session`. With a dedup path set, `sync_repo(agent)` / `hydrate_from_jetstream(agent)` keep the dedup + realtime store warm. **Identity hashes**: `song_hash(title, artist, album)`, `artist_hash(album_artist)`. # Go Source: https://docs.rocksky.app/sdks/go Go SDK for Rocksky — built on bluesky-social/indigo. The Go SDK is built on [indigo](https://github.com/bluesky-social/indigo): `Client` does unauthenticated AppView reads, and `Agent` logs in with an app password and writes `app.rocksky.*` records to the user's PDS. It also ships a local dedup index and Jetstream real-time sync. ## Install ```bash theme={null} go get github.com/tsirysndr/rocksky/sdk/go ``` ## Quickstart ```go theme={null} import ( "context" "github.com/tsirysndr/rocksky/sdk/go/rocksky" "github.com/tsirysndr/rocksky/sdk/go/rocksky/gen" ) ctx := context.Background() // Reads — unauthenticated. NewClient("") uses https://api.rocksky.app. c := rocksky.NewClient("") stats, _ := c.GlobalStats(ctx) top, _ := c.TopTracks(ctx, 10, 0) month, _ := c.TopTracksInterval(ctx, rocksky.LastDays(30), 50, 0) // Writes — log in with an app password (resolves the PDS automatically). agent, _ := rocksky.Login(ctx, "alice.bsky.social", "app-password") uri, _ := agent.Scrobble(ctx, gen.ScrobbleRecord{ Title: "Chaser", Artist: "Calibro 35", Album: "Jazzploitation", AlbumArtist: "Calibro 35", Duration: 182320, }) ``` ## API **Reads — `Client`**: the full `app.rocksky.*` read surface. Basics: `Profile`, `Scrobbles`, `Songs`, `Albums`, `Artists`, `TopTracks`, `TopArtists`, `Search`, `GlobalStats`. Catalog & relations: `CatalogAlbums`, `CatalogArtists`, `CatalogSongs`, `AlbumTracks`, `ArtistAlbums`, `ArtistTracks`, `LovedSongs`, `ScrobbleFeed`, `Scrobble` (single by uri), `Follows`, `Followers`, `KnownFollowers`, plus raw `json.RawMessage` detail methods (`Album`, `Artist`, `Song`, `Playlists`, `Playlist`, `Stats`, `Wrapped`, `ScrobblesChart`, `Recommendations`, `Neighbours`, `Shouts`, and more). `Get(ctx, nsid, params)` calls any read query by nsid and returns raw `json.RawMessage` — every named method is sugar over it. `MatchSong(ctx, title, artist, mbID, isrc)` resolves a bare title + artist into full canonical metadata. Attach a bearer token for auth-gated queries with `NewClient("").WithToken(token)`. **Date-window charts**: `TopTracksInterval` / `TopArtistsInterval` take a typed `DateInterval`, built with `rocksky.AllTime()`, `rocksky.LastDays(n)`, `LastWeeks`, `LastMonths`, `LastYears`, or `rocksky.Range(start, end)`; plain `TopTracks` / `TopArtists` remain all-time shorthands. **Writes — `Agent`**: `Scrobble` (fan-out from full metadata), `ScrobbleMatch(ctx, appview, ScrobbleMatchInput{Title, Artist})` (resolves metadata via `MatchSong`, then the same fan-out; Title/Artist required, Album/MbID/ISRC/Timestamp optional pointers, `appview` "" = default), `CreateSong` / `CreateAlbum` / `CreateArtist`, `Like`, `Follow`, `Shout` / `ReplyShout`, `SetNowPlaying` / `ClearNowPlaying`, `Delete`, `RefreshSession`. Records are the generated `gen.*Record` types. **Identity hashes**: `SongHash`, `AlbumHash`, `ArtistHash`. ## Duplicate prevention & real-time sync An optional local index (embedded bbolt KV — no cgo/RocksDB) prevents duplicate writes and stays live off the firehose: ```go theme={null} idx, _ := rocksky.OpenIndex("dedup.db") defer idx.Close() agent.UseIndex(idx) agent.SyncRepo(ctx) // backfill from the repo CAR go agent.HydrateFromJetstream(ctx) // keep it live from Jetstream (all 4 servers) ``` # Kotlin Source: https://docs.rocksky.app/sdks/kotlin Kotlin SDK for Rocksky — native core via UniFFI (JNA). `app.rocksky:rocksky-kotlin` binds the shared Rocksky Rust core via UniFFI (JNA-loaded), package `app.rocksky`: AppView reads, AT Protocol PDS writes, a local dedup index, and the identity hashes — the same engine behind every Rocksky SDK. ## Install ```kotlin theme={null} dependencies { implementation("app.rocksky:rocksky-kotlin:0.7.0") } ``` The published jar bundles the native library for each platform. ## Quickstart ```kotlin theme={null} import app.rocksky.* // Reads — unauthenticated. AppView() uses https://api.rocksky.app. val av = AppView() println(av.globalStats().scrobbles) av.topTracks(10u, 0u).forEach { println("${it.artist} — ${it.title}") } // Writes — log in with an app password. val agent = login("session.json", "alice.bsky.social", "app-password") val out = agent.scrobble(ScrobbleInput( title = "Chaser", artist = "Calibro 35", album = "Jazzploitation", albumArtist = "Calibro 35", durationMs = 182320, )) println(out.scrobbleUri) ``` ## API **Reads — `AppView(base: String? = null, token: String? = null)`** (pass `token` to send `Authorization: Bearer` on auth-gated queries; counts are `UInt`). Typed views: `profile`, `scrobbles`, `songs`, `albums`, `artists`, `lovedSongs`, `catalogAlbums` / `catalogArtists` / `catalogSongs`, `albumTracks`, `artistAlbums` / `artistTracks`, `scrobbleFeed`, `scrobble` (single), `follows` / `followers` / `knownFollowers`, `topTracks`, `topArtists`, `globalStats`. Date-window charts `topTracksInterval(limit, offset, interval)` and `topArtistsInterval(...)` take a `DateInterval` (`AllTime`, `LastDays(7u)`, `LastWeeks`, `LastMonths`, `LastYears`, `Range(start, end)`). `matchSong(title, artist, mbId, isrc)` resolves a bare title+artist to canonical metadata. A raw-JSON long tail (`album`, `artist`, `song`, `playlists`, `playlist`, `stats`, `wrapped`, `scrobblesChart`, `recommendations`, `neighbours`, shouts, …) returns JSON strings, and `av.get(nsid, mapOf(...))` calls **any** read query by NSID. **Writes — `Agent`**: `login(...)` (top-level, `appview`/`dedupPath` optional), then `scrobble` (full metadata) or `scrobbleMatch(ScrobbleMatchInput(title, artist, album, mbId, isrc, timestamp))` (resolve via `matchSong`, then fan out — `title`/`artist` required, `album`/`mbId`/`isrc` are `String?` and `timestamp` is `Long?`, all default null; `ScrobbleMatchInput` is an `app.rocksky` typealias), `createSong` / `createAlbum` / `createArtist`, `like` / `unlike`, `follow` / `unfollow`, `shout` / `replyShout`, `setNowPlaying` / `clearNowPlaying`, `syncRepo` (dedup backfill, returns per-collection counts as JSON), `hydrateFromJetstream` (live dedup off the firehose). Write verbs throw `RockskyException`. **Identity hashes**: `songHash`, `albumHash`, `artistHash`. # SDKs Source: https://docs.rocksky.app/sdks/overview Official Rocksky SDKs — native AT Protocol clients across ten languages. The Rocksky SDKs are **native AT Protocol clients**: they read from the public AppView and write `app.rocksky.*` records straight to the user's PDS (scrobbles, likes, follows, shouts, now-playing). They are not thin HTTP wrappers. Two implementations share one behaviour: * **Shared Rust core** — Python, Ruby, Kotlin, Clojure, and the BEAM trio (Erlang/Elixir/Gleam) are thin bindings over the same Rust engine (`rocksky-sdk`), so auth, record writing, dedup, and the identity hashes are identical everywhere. * **Native ecosystem** — Go is built on [indigo](https://github.com/bluesky-social/indigo), TypeScript on [atcute](https://github.com/mary-ext/atcute), and Rust is the `rocksky-sdk` crate itself (on [jacquard](https://crates.io/crates/jacquard)). `@rocksky/sdk` — built on atcute `rocksky` — native core bindings `rocksky-sdk` crate — jacquard `sdk/go/rocksky` — built on indigo `rocksky` gem — native core (fiddle) `app.rocksky:rocksky-kotlin` — UniFFI `rocksky_ex` — over the `rocksky_erl` NIF `rocksky_erl` — Rustler NIF `app.rocksky/sdk` — Panama FFM `rocksky` — over the `rocksky_erl` NIF ## Common surface Every SDK exposes the same three things: * **Reads** — a client over the public AppView (`https://api.rocksky.app`, overridable) covering the full `app.rocksky.*` read catalog: `globalStats`, `topTracks`, `topArtists`, `profile`, `scrobbles`, `songs`, `albums`, `artists`, `search`, catalog/loved/follow queries, plus a universal `get(nsid, params)` escape hatch that calls any read query by nsid. Charts take a typed `DateInterval` (all-time, last N days/weeks/months/years, or a custom range), and `matchSong` resolves a bare title + artist into full canonical metadata. Each read client also accepts an optional **bearer access token** for auth-gated queries. * **Writes** — an **Agent** that logs in with an **app password**, resolves the account's PDS, and writes records: `scrobble`, `scrobble_match` (match-then-write from a title/artist input object, with optional album override, `mbId`/`isrc` match anchors, and a scrobbled-at timestamp), `createSong` / `createAlbum` / `createArtist`, `like`, `follow`, `shout`, now-playing. * **Identity hashes** — `songHash` / `albumHash` / `artistHash` (lowercase-hex SHA-256), byte-for-byte identical across every SDK and the server. ## Duplicate prevention & real-time sync The Rust, Go, and TypeScript SDKs (and the native-core bindings) ship an optional **local dedup index** keyed by the identity hashes: `syncRepo()` backfills it from the repo CAR (`com.atproto.sync.getRepo`), and `hydrateFromJetstream()` keeps it live off the Bluesky Jetstream firehose. With an index attached, write verbs skip records that already exist and never duplicate a same-second scrobble. ## Choosing an SDK | You're writing… | Use | | ------------------------------------------------- | -------------------------------------------------- | | A web app, Cloudflare Worker, or Node/Bun service | [TypeScript](/sdks/typescript) | | A backend job, notebook, or data pipeline | [Python](/sdks/python) | | A performance-critical service or CLI | [Rust](/sdks/rust) or [Go](/sdks/go) | | A JVM app | [Kotlin](/sdks/kotlin) or [Clojure](/sdks/clojure) | | A BEAM app | [Elixir](/sdks/elixir) or [Gleam](/sdks/gleam) | | A Ruby app or script | [Ruby](/sdks/ruby) | # Python Source: https://docs.rocksky.app/sdks/python Python SDK for Rocksky — native bindings to the shared Rust core. `rocksky` is native bindings to the shared Rocksky Rust core (via UniFFI): AppView reads, AT Protocol PDS writes, a local dedup index, and the identity hashes — the same engine behind every Rocksky SDK. ## Install ```bash uv theme={null} uv add rocksky==0.6.0 ``` ```bash pip theme={null} pip install rocksky==0.6.0 ``` The wheel is pure-Python; the native library is fetched from the GitHub release on first import and cached. ## Quickstart ```python theme={null} from rocksky import AppView, Agent, ScrobbleInput, song_hash # Reads — unauthenticated. AppView() uses https://api.rocksky.app. av = AppView() print(av.global_stats().scrobbles) for t in av.top_tracks(10, 0): print(t.artist, "—", t.title) # Writes — log in with an app password. agent = Agent.login_password("session.json", "alice.bsky.social", "app-password", None, None) out = agent.scrobble(ScrobbleInput( title="Chaser", artist="Calibro 35", album="Jazzploitation", album_artist="Calibro 35", duration_ms=182320, )) print(out.scrobble_uri) ``` ## API **Reads — `AppView(base=None, token=None)`** (pass `token` to send `Authorization: Bearer` on auth-gated queries). Typed views: `profile`, `scrobbles`, `songs`, `albums`, `artists`, `loved_songs`, `catalog_albums` / `catalog_artists` / `catalog_songs`, `album_tracks`, `artist_albums` / `artist_tracks`, `scrobble_feed`, `scrobble` (single), `follows` / `followers` / `known_followers`, `top_tracks`, `top_artists`, `global_stats`. Date-window charts `top_tracks_interval(limit, offset, interval)` and `top_artists_interval(...)` take a `DateInterval` (`ALL_TIME`, `LAST_DAYS(days=7)`, `LAST_WEEKS`, `LAST_MONTHS`, `LAST_YEARS`, `RANGE(start=..., end=...)`). `match_song(title, artist, mb_id, isrc)` resolves a bare title+artist to canonical metadata. A raw-JSON long tail (`album`, `artist`, `song`, `playlists`, `playlist`, `stats`, `wrapped`, `scrobbles_chart`, `recommendations`, `neighbours`, shouts, …) returns JSON strings, and `av.get(nsid, params)` (a `dict[str, str]`) calls **any** read query by NSID. **Writes — `Agent`**: `login_password(...)`, then `scrobble` (full metadata) or `scrobble_match(ScrobbleMatchInput(title, artist, album=None, mb_id=None, isrc=None, timestamp=None))` (resolve via `match_song`, then fan out — `title`/ `artist` required, `timestamp` is scrobbled-at Unix seconds; `ScrobbleMatchInput` imports from `rocksky`), `create_song` / `create_album` / `create_artist`, `like` / `unlike`, `follow` / `unfollow`, `shout` / `reply_shout`, `set_now_playing` / `clear_now_playing`, `sync_repo` (dedup backfill, returns per-collection counts as JSON), `hydrate_from_jetstream` (live dedup off the firehose). **Identity hashes**: `song_hash`, `album_hash`, `artist_hash`. ## Duplicate prevention Pass a `dedup_path` to `Agent.login_password` to enable the local index, then `agent.sync_repo()` to backfill it from the repo. With it, write verbs skip records that already exist. # Ruby Source: https://docs.rocksky.app/sdks/ruby Ruby SDK for Rocksky — native bindings to the shared Rust core. `rocksky` binds the shared Rocksky Rust core through Ruby's stdlib `fiddle` (no `ffi` gem): AppView reads, AT Protocol PDS writes, and the identity hashes — the same engine behind every Rocksky SDK. ## Install ```bash theme={null} gem install rocksky ``` Or in a Gemfile: `gem "rocksky", "~> 0.6"`. The gem is pure-Ruby; the native library is fetched from the GitHub release on first load and cached. ## Quickstart ```ruby theme={null} require "rocksky" # Reads — unauthenticated. base: overrides https://api.rocksky.app. stats = Rocksky.global_stats puts stats["scrobbles"] Rocksky.top_tracks(limit: 10).each { |t| puts "#{t["artist"]} — #{t["title"]}" } # Writes — log in with an app password. agent = Rocksky::Agent.login("session.json", "alice.bsky.social", "app-password") out = agent.scrobble( "title" => "Chaser", "artist" => "Calibro 35", "album" => "Jazzploitation", "albumArtist" => "Calibro 35", "durationMs" => 182_320 ) puts out["scrobbleUri"] agent.close ``` ## API Reads/writes return plain Hashes; write verbs raise `Rocksky::Error` on failure. Records are Hashes with camelCase keys. **Reads — `Rocksky`**: named reads `profile(actor, base:)`, `scrobbles(actor, limit:, offset:, base:)`, `top_tracks(limit:, offset:, base:)`, `global_stats(base:)`. **Universal escape hatch**: `Rocksky.get(nsid, params, base: nil, token: nil)` reaches the whole `app.rocksky.*` read catalog and returns a parsed Hash/Array — e.g. `"app.rocksky.album.getAlbums"`, `"app.rocksky.album.getAlbumTracks"`, `"app.rocksky.graph.getFollows"`, `"app.rocksky.actor.getActorLovedSongs"`, `"app.rocksky.stats.getStats"`, `"app.rocksky.charts.getScrobblesChart"`. Pass `token:` for an `Authorization: Bearer` header on auth-gated queries. **Typed date-window charts**: `top_tracks_interval(limit:, offset:, interval:)` and `top_artists_interval(...)` — `interval:` is `:all` or `[:days, n]` / `[:weeks, n]` / `[:months, n]` / `[:years, n]` / `[:range, start, end]`, e.g. `Rocksky.top_tracks_interval(limit: 5, interval: [:days, 7])`. **Match**: `Rocksky.match_song(title, artist, mb_id: nil, isrc: nil)` resolves a bare title + artist into full canonical metadata. **Writes — `Rocksky::Agent`**: `login(..., dedup_path:)`, then `scrobble` (full metadata) or `scrobble_match(params)` — a single Hash with camelCase string keys, required `"title"`/`"artist"`, optional `"album"`, `"mbId"`, `"isrc"`, `"timestamp"` (match-then-write), plus `like`, `follow`, `shout`, `refresh_session`, `close`. With `dedup_path:` set at login, `sync_repo` (repo backfill) and `hydrate_from_jetstream` (live stream) run deduped against the on-disk store. **Identity hashes**: `Rocksky.song_hash(title, artist, album)`. # Rust Source: https://docs.rocksky.app/sdks/rust Rust SDK for Rocksky — the rocksky-sdk crate, built on jacquard. `rocksky-sdk` is the Rust SDK and the shared engine behind every other Rocksky SDK. Built on [jacquard](https://crates.io/crates/jacquard), it mirrors Bluesky's `@atproto/api`: `RockskyAgent` wraps a session and exposes high-level write verbs; reads go through the unauthenticated `AppView`. It also ships an optional dedup index and Jetstream real-time sync. ## Install ```toml theme={null} [dependencies] rocksky-sdk = { version = "0.4", features = ["dedup", "jetstream"] } ``` The `dedup` and `jetstream` features are optional (they pull in RocksDB + the firehose client). ## Quickstart ```rust theme={null} use rocksky_sdk::{RockskyAgent, ScrobbleDraft}; let agent = RockskyAgent::builder() .session_store("~/.config/rocksky/session.json") .build()?; agent.login_password("alice.bsky.social", "app-password").await?; // Reads — via the bundled AppView client. let recent = agent.appview().scrobbles("alice.bsky.social", 25, 0).await?; // Writes — the scrobble fans out to artist/album/song then the scrobble. agent.scrobble(&ScrobbleDraft { title: "Chaser".into(), artist: "Calibro 35".into(), album: "Jazzploitation".into(), album_artist: "Calibro 35".into(), duration_ms: 182_320, ..Default::default() }).await?; ``` ## API **Reads — `AppView`**: the full `app.rocksky.*` read surface. Basics: `profile`, `scrobbles`, `songs`, `albums`, `artists`, `feed`, `search`, `top_artists`, `top_tracks`, `global_stats`. Catalog & relations: `catalog_albums`, `catalog_artists`, `catalog_songs`, `album_tracks`, `artist_albums`, `artist_tracks`, `loved_songs`, `scrobble_feed`, `scrobble` (single by uri), `follows`, `followers`, `known_followers`. Raw-JSON detail/long-tail methods: `album`, `artist`, `song`, `playlists`, `playlist`, `stats`, `wrapped`, `scrobbles_chart`, `recommendations`, `neighbours`, `shouts`, and more. `get(nsid, ¶ms)` is the universal escape hatch — calls any read query by nsid and returns raw `serde_json::Value`; every named method is sugar over it. `match_song(title, artist, mb_id, isrc)` resolves a bare title + artist into full canonical metadata. Attach a bearer token for auth-gated queries with `AppView::new(base).with_token(token)` (or `set_token`). **Date-window charts**: `top_tracks_interval` / `top_artists_interval` take a typed `DateInterval` — `AllTime`, `LastDays(n)`, `LastWeeks(n)`, `LastMonths(n)`, `LastYears(n)`, or `Range { start, end }`; `top_tracks` / `top_artists` remain all-time shorthands. ```rust theme={null} use rocksky_sdk::DateInterval; let month = av.top_tracks_interval(DateInterval::LastDays(30), 50, 0).await?; ``` **Writes — `RockskyAgent`**: `scrobble` (fan-out from full metadata), `scrobble_match(&ScrobbleMatch { .. })` (resolves metadata via `match_song`, then the same fan-out; title/artist required, album/mb\_id/isrc/timestamp optional), `create_song` / `create_album` / `create_artist`, `like` / `unlike`, `follow` / `unfollow`, `shout` / `reply_shout`, `set_now_playing` / `clear_now_playing`. App-password + loopback OAuth login. **Identity hashes**: `rocksky_sdk::dedup::{song_hash, album_hash, artist_hash}`. ## Duplicate prevention & real-time sync With the `dedup` feature, attach a RocksDB-backed index; with `jetstream`, keep it live off the firehose: ```rust theme={null} let agent = RockskyAgent::builder() .session_store("session.json") .dedup_store("dedup") // requires the `dedup` feature .build()?; agent.sync_repo().await?; // backfill from the repo CAR tokio::spawn(async move { agent.hydrate_from_jetstream().await }); ``` # TypeScript Source: https://docs.rocksky.app/sdks/typescript TypeScript SDK for Rocksky — built on atcute. `@rocksky/sdk` is built on [atcute](https://github.com/mary-ext/atcute): `RockskyClient` does unauthenticated AppView reads, and `Agent` logs in with an app password and writes `app.rocksky.*` records to the user's PDS. It also ships a local dedup index and Jetstream real-time sync. Version 0.4.0 is a **breaking** rewrite on atcute — it is a native AT Protocol client, not the old HTTP wrapper. See the package CHANGELOG. ## Install ```bash npm theme={null} npm install @rocksky/sdk ``` ```bash bun theme={null} bun add @rocksky/sdk ``` Requires Node ≥ 22 (global `WebSocket` / `fetch`) or Bun. ## Quickstart ```ts theme={null} import { RockskyClient, Agent } from "@rocksky/sdk"; // Reads — unauthenticated. new RockskyClient() uses https://api.rocksky.app. const rk = new RockskyClient(); const stats = await rk.globalStats(); const top = await rk.topTracks(10, 0); // Writes — log in with an app password (resolves the PDS automatically). const agent = await Agent.login("alice.bsky.social", "app-password"); const uri = await agent.scrobble({ title: "Chaser", artist: "Calibro 35", album: "Jazzploitation", albumArtist: "Calibro 35", duration: 182320, }); ``` ## API **Reads — `RockskyClient`**: the client now covers the whole `app.rocksky.*` read surface. Typed methods include `profile`, `scrobbles`, `songs`, `albums`, `artists`, `topTracks`, `topArtists`, `search`, `globalStats`, `lovedSongs`, `catalogAlbums`, `catalogArtists`, `catalogSongs`, `albumTracks`, `artistAlbums`, `artistTracks`, `scrobbleFeed`, `scrobble` (single by uri), `follows`, `followers`, `knownFollowers`. Raw (`unknown`-returning) detail / long-tail methods cover the rest: `album`, `artist`, `song`, `feed`, `playlists`, `playlist`, `stats`, `wrapped`, `scrobblesChart`, `recommendations`, `neighbours`, shouts, and more. Every named method is sugar over the universal escape hatch **`rk.get(nsid, params)`**, which calls ANY read query by nsid and returns `unknown`. **Typed date-window charts**: `topTracksInterval(limit, offset, interval)` and `topArtistsInterval(...)` take a `DateInterval` built with the `Interval` factories — `Interval.allTime()`, `Interval.lastDays(n)`, `Interval.lastWeeks(n)`, `Interval.lastMonths(n)`, `Interval.lastYears(n)`, `Interval.range(start, end)`. `topTracks` / `topArtists` remain all-time shorthands. ```ts theme={null} import { RockskyClient, Interval } from "@rocksky/sdk"; const rk = new RockskyClient(); const monthly = await rk.topTracksInterval(10, 0, Interval.lastMonths(1)); // Resolve a bare title + artist into full canonical metadata. const song = await rk.matchSong("Chaser", "Calibro 35"); ``` **`matchSong(title, artist, mbId?, isrc?)`**: resolves a bare title + artist into full canonical metadata (album, artwork, duration, MBID, ISRC, links). **Auth-gated reads**: pass an optional bearer access token — `new RockskyClient(appview, token)` — sent as `Authorization: Bearer `. **Writes — `Agent`**: two scrobble paths — `scrobble(rec)` writes full metadata you already have, and `scrobbleMatch(input, appview?)` takes a `ScrobbleMatchInput` object (`{ title, artist, album?, mbId?, isrc?, timestamp? }` — title/artist required; `album` overrides the resolved album, `mbId`/`isrc` are match anchors, `timestamp` is a scrobbled-at Unix-seconds time; the optional `appview` overrides the AppView used for matching) and resolves full metadata via `matchSong` first, then writes — e.g. `agent.scrobbleMatch({ title: "Chaser", artist: "Calibro 35" })`. Plus `createSong` / `createAlbum` / `createArtist`, `like`, `follow`, `shout` / `replyShout`, `setNowPlaying` / `clearNowPlaying`, `delete`. **Identity hashes**: `songHash`, `albumHash`, `artistHash`. ## Duplicate prevention & real-time sync An optional local index (embedded classic-level LevelDB) prevents duplicate writes and stays live off the firehose: ```ts theme={null} import { RockskyIndex } from "@rocksky/sdk"; const idx = new RockskyIndex("./dedup"); await idx.open(); agent.useIndex(idx); await agent.syncRepo(); // backfill from the repo CAR agent.hydrateFromJetstream(); // keep it live from Jetstream (all 4 servers) ```