1. developing plugins
  2. implementing endpoints

Implementing Endpoints

If your plugin doesn’t need authentication support, you can skip this section.

In the src/segments/auth.ht file you can find all the required method definition. These are the necessary methods Spotube calls in it’s lifecycle.

class AuthEndpoint {
  var client: HttpClient
  final controller: StreamController

  get authStateStream -> Stream => controller.stream

  construct (this.client){
    controller = StreamController.broadcast()
  }

  fun isAuthenticated() -> bool {
    // TODO: Implement method
    return false
  }

  fun authenticate() -> Future {
    // TODO: Implement method
  }

	fun logout() -> Future {
		// TODO: Implement method
	}
}

For this specific endpoint, you may need WebView or Forms to get user inputs. The hetu_spotube_plugin provides such APIs.

Learn more about it in the Spotube Plugin API section

The AuthEndpoint.authStateStream property is also necessary to notify Spotube about the authentication status. hetu_std is a built-in module and it exports StreamController which basically 1:1 copy of the Dart’s StreamController. If the status of authentication changes you need to add a new event using the controller.add Following events are respected by Spotube:

NameDescription
loginWhen user successfully completes login
logoutWhen user logs out of the service
recoveredWhen user’s cached/saved credentials are recovered from disk
refreshedWhen user’s session is refreshed

Example of adding a new authentication event:

controller.add({ type: "login" }.toJson())

By the way, the event type is a Map<String, dynamic> in the Dart side, so make sure to always convert hetu_script’s structs into Maps

The UserEndpoint is used to fetch user information and manage user-related actions. In the src/segments/user.ht file you can find all the required method definitions. These are the necessary methods Spotube calls in its lifecycle.

Most of these methods should be just a mapping to an API call with minimum latency. Avoid calling plugin APIs like WebView or Forms in these methods. User interactions should be avoided here generally.

class UserEndpoint {
  var client: HttpClient

  construct (this.client)

  fun me() {
    // TODO: Implement method
  }

  fun savedTracks({ offset: int, limit: int }) {
    // TODO: Implement method
  }

  fun savedPlaylists({ offset: int, limit: int }) {
    // TODO: Implement method
  }

  fun savedAlbums({ offset: int, limit: int }) {
    // TODO: Implement method
  }

  fun savedArtists({ offset: int, limit: int }) {
    // TODO: Implement method
  }

  fun isSavedPlaylist(playlistId: string) { // Future<bool>
    // TODO: Implement method
  }

  fun isSavedTracks(trackIds: List) { // Future<List<bool>>
    // TODO: Implement method
  }

  fun isSavedAlbums(albumIds: List) { // Future<List<bool>>
    // TODO: Implement method
  }

  fun isSavedArtists(artistIds: List) { // Future<List<bool>>
    // TODO: Implement method
  }
}

These methods are pretty self-explanatory. You need to implement them to fetch user information from your service.

MethodDescriptionReturns
me()Fetches the current user’s information.SpotubeUserObject
savedTracks()Fetches the user’s saved tracks with pagination support.SpotubePaginationResponseObject of SpotubeFullTrackObject
savedPlaylists()Fetches the user’s saved playlists with pagination support.SpotubePaginationResponseObject of SpotubeFullPlaylistObject
savedAlbums()Fetches the user’s saved albums with pagination support.SpotubePaginationResponseObject of SpotubeFullAlbumObject
savedArtists()Fetches the user’s saved artists with pagination support.SpotubePaginationResponseObject of SpotubeFullArtistObject
isSavedPlaylist()Checks if a playlist is saved by the user. Returns a Future<bool>.bool
isSavedTracks()Checks if tracks are saved by the user. Returns a Future<List<bool>>.List<bool> (each boolean corresponds to a track ID)
isSavedAlbums()Checks if albums are saved by the user. Returns a Future<List<bool>>.List<bool> (each boolean corresponds to an album ID)
isSavedArtists()Checks if artists are saved by the user. Returns a Future<List<bool>>.List<bool> (each boolean corresponds to an artist ID)

Note: The isSavedTracks, isSavedAlbums, and isSavedArtists methods accept a list of IDs and return a list of booleans indicating whether each item is saved by the user. The order of the booleans in the list corresponds to the order of the IDs in the input list.

The TrackEndpoint is used to fetch track information and do track-related actions. In the src/segments/track.ht file you can find all the required method definitions.

class TrackEndpoint {
  var client: HttpClient

  construct (this.client)

  fun getTrack(id: string) {
    // TODO: Implement method
  }

  fun save(trackIds: List) { // List<String>
    // TODO: Implement method
  }

  fun unsave(trackIds: List) { // List<String>
    // TODO: Implement method
  }

  fun radio(id: string) {
    // TODO: Implement method
  }
}
MethodDescriptionReturns
getTrack()Fetches track information by ID.SpotubeFullTrackObject
save()Saves the specified tracks. Accepts a list of track IDs.void
unsave()Removes the specified tracks from saved tracks. Accepts a list of track IDs.void
radio()Fetches related tracks based on specified tracks. Try to return a List of 50 tracks.List<SpotubeFullTrackObject>

The AlbumEndpoint is used to fetch album information and do album-related actions. In the src/segments/album.ht file you can find all the required method definitions.

class AlbumEndpoint {
  construct (this.client)

  fun getAlbum(id: string) {
    // TODO: Implement method
  }

  fun tracks(id: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun releases({offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun save(albumIds: List) { // List<String>
    // TODO: Implement method
  }

  fun unsave(albumIds: List) { // List<String>
    // TODO: Implement method
  }
}
MethodDescriptionReturns
getAlbum()Fetches album information by ID.SpotubeFullAlbumObject
tracks()Fetches tracks of the specified album. Accepts an ID and optional pagination parameters.SpotubePaginationResponseObject of SpotubeFullTrackObject
releases()Fetches new album releases user followed artists or globallySpotubePaginationResponseObject of SpotubeFullAlbumObject
save()Saves the specified albums. Accepts a list of album IDs.void
unsave()Removes the specified albums from saved albums. Accepts a list of album IDs.void

The ArtistEndpoint is used to fetch artist information and do artist-related actions. In the src/segments/artist.ht file you can find all the required method definitions.

class ArtistEndpoint {
  var client: HttpClient

  construct (this.client)

  fun getArtist(id: string) {
    // TODO: Implement method
  }

  fun related(id: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun topTracks(id: string, {limit: int, offset: int}) {
    // TODO: Implement method
  }

  fun albums(id: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun save(artistIds: List) {
    // TODO: Implement method
  }

  fun unsave(artistIds: List) {
    // TODO: Implement method
  }
}
MethodDescriptionReturns
getArtist()Fetches artist information by ID.SpotubeFullArtistObject
related()Fetches related artists based on the specified artist ID. Accepts optional pagination.SpotubePaginationResponseObject of SpotubeFullArtistObject
topTracks()Fetches top tracks of the specified artist. Accepts optional pagination.SpotubePaginationResponseObject of SpotubeFullTrackObject
albums()Fetches albums of the specified artist. Accepts optional pagination.SpotubePaginationResponseObject of SpotubeFullAlbumObject
save()Saves the specified artists. Accepts a list of artist IDs.void
unsave()Removes the specified artists from saved artists. Accepts a list of artist IDs.void

The PlaylistEndpoint is used to fetch playlist information and do track-related actions. In the src/segments/playlist.ht file you can find all the required method definitions.

class PlaylistEndpoint {
  var client: HttpClient

  construct (this.client)

  fun getPlaylist(id: string) {
    // TODO: Implement method
  }

  fun tracks(id: string, { offset: int, limit: int }) {
    // TODO: Implement method
  }

  fun create(userId: string, {
    name: string,
    description: string,
    public: bool,
    collaborative: bool
  }) {
    // TODO: Implement method
  }

  fun update(playlistId: string, {
    name: string,
    description: string,
    public: bool,
    collaborative: bool
  }) {
    // TODO: Implement method
  }

  fun deletePlaylist(playlistId: string) {
    // TODO: Implement method
  }

  fun addTracks(playlistId: string, { trackIds: List, position: int }) {
    // TODO: Implement method
  }


  fun removeTracks(playlistId: string, { trackIds: List }) {
    // TODO: Implement method
  }

  fun save(playlistId: string) {
    // TODO: Implement method
  }

  fun unsave(playlistId: string) {
    // TODO: Implement method
  }
}
MethodDescriptionReturns
getPlaylistFetches a playlist by its ID.SpotubeFullPlaylistObject
tracksFetches tracks in a playlist.SpotubePaginationResponseObject of SpotubeFullTrackObject
createCreates a new playlist and returnsSpotubeFullPlaylistObject
updateUpdates an existing playlist.void
deletePlaylistDeletes a playlist.void
addTracksAdds tracks to a playlist.void
removeTracksRemoves tracks from a playlist.void
saveSaves a playlist to the user’s library.void
unsaveRemoves a playlist from the user’s library.void

The SearchEndpoint is used to fetch search playlist, tracks, album and artists. In the src/segments/search.ht file you can find all the required method definitions.

class SearchEndpoint {
  var client: HttpClient

  construct (this.client)

  get chips -> List { // Set<string>
    // can be tracks, playlists, artists, albums and all
    return ["all", "tracks", "albums", "artists", "playlists"]
  }

  fun all(query: string) {
    // TODO: Implement method
  }

  fun albums(query: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun artists(query: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun tracks(query: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }

  fun playlists(query: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }
}
MethodDescriptionReturns
chipsReturns the available search chips.List<string>
all()Searches for all types of content.SpotubeSearchResponseObject
albums()Searches only for albums.SpotubePaginationResponseObject of SpotubeFullAlbumObject
artists()Searches only for artists.SpotubePaginationResponseObject of SpotubeFullArtistObject
tracks()Searches only for tracks.SpotubePaginationResponseObject of SpotubeFullTrackObject
playlists()Searches only for playlists.SpotubePaginationResponseObject of SpotubeFullPlaylistObject

The BrowseEndpoint is used to fetch recommendations and catalogs of playlists, albums and artists. In the src/segments/browse.ht file you can find all the required method definitions.

class BrowseEndpoint {
  var client: HttpClient

  construct (this.client)

  fun sections({offset: int, limit: int}) {
   // TODO: Implement method
  }

  fun sectionItems(id: string, {offset: int, limit: int}) {
    // TODO: Implement method
  }
}
MethodDescriptionReturns
sections()Returns the sections of the home page.SpotubePaginationResponseObject of SpotubeBrowseSectionObject of Object
sectionItems()Returns the items of a specific section.SpotubePaginationResponseObject of Object

In sectionItems() The id it takes comes from sections(). It is basically used in an expanded screen to show the browse section items with pagination.

For sections returned by sections() if browseMore is true that’s when sectionItems() is used to fetch the items of that section.

By the way, the Object can be any of the following types:

The CoreEndpoint is a special subclass which is used to check update and scrobbling and to get support text. In the src/segments/core.ht file you can find all the required method definitions.

class CorePlugin {
  var client: HttpClient

  construct (this.client)

  /// Checks for updates to the plugin.
  /// [currentConfig] is just plugin.json's file content.
  ///
  /// If there's an update available, it will return a map of:
  /// - [downloadUrl] -> direct download url to the new plugin.smplug file.
  /// - [version] of the new plugin.
  /// - [changelog] Optionally, a changelog for the update (markdown supported).
  ///
  /// If no update is available, it will return null.
  fun checkUpdate(currentConfig: Map) -> Future {
    // TODO: Check for updates
  }

  /// Returns the support information for the plugin in Markdown or plain text.
  /// Supports images and links.
  get support -> string {
    // TODO: Return support information
    return ""
  }

  /// Scrobble the provided details to the scrobbling service supported by the plugin.
  /// "scrobbling" must be set as an ability in the plugin.json
  /// [details] is a map containing the scrobble information, such as:
  /// - [id] -> The unique identifier of the track.
  /// - [title] -> The title of the track.
  /// - [artists] -> List of artists
  ///   - [id] -> The unique identifier of the artist.
  ///   - [name] -> The name of the artist.
  /// - [album] -> The album of the track
  ///   - [id] -> The unique identifier of the album.
  ///   - [name] -> The name of the album.
  /// - [timestamp] -> The timestamp of the scrobble (optional).
  /// - [duration_ms] -> The duration of the track in milliseconds (optional).
  /// - [isrc] -> The ISRC code of the track (optional).
  fun scrobble(details: Map) {
    // TODO: Implement scrobbling
  }
}
MethodDescriptionReturns
checkUpdate()Checks for updates to the plugin.Future with a map containing downloadUrl, version, and optionally changelog. If no update is available, returns null.
supportReturns support information.string containing the support information in Markdown or plain text.
scrobble()Scrobbles the provided track details. This is only called if your plugin.json has scrobbling in the abilities fieldvoid

In the checkUpdate() method the plugin.json’s content will be passed as map. You can use that to check updates using the version field.

Also, the downloadUrl it provides should be a direct binary download link (redirect is supported) for the .smplug file