Introduction

Moosync lets you create your own extensions to extent the functionality for the main app. Extensions are web-assembly modules that can be loaded at runtime.

Restrictions

Web assembly is platform independant and has a few restrictions:

  • Limited access to the file system
  • Limited access to the network
  • No support for threads (tracking wasi-threads)
  • No support for native libraries

Development

Moosync uses extism under the hood to load web assembly modules. Any host language supported by extism can be used to write extensions. Currently we only provide support for

  • Rust
  • Golang
  • Javascript / Typescript
  • Python

Getting started

Extensions follow a very specific layout.

Creating a new extension

Manifest

Create a new file called package.json. This will be the manifest for your extension containing all details and permissions about it.

A sample manifest looks like

{
  "name": "moosync.sample.extension",
  "version": "0.0.1",
  "icon": "assets/icon.svg",
  "extensionEntry": "ext.wasm",
  "moosyncExtension": true,
  "displayName": "My extension",
  "permissions": {
    "hosts": [
      "*.google.com",
      "google.com"
    ],
    "paths": {
      "{ENV_1}": "/",
    }
  }
}

Fields

  • name: A unique identifier for your extension.
  • version: The version of your extension in semver format.
  • icon: The icon of your extension. The path is relative to package.json.
  • extensionEntry: The path to the compiled WASM file. The path is relative to package.json.
  • moosyncExtension: This must be set to true for the extension to be loaded.
  • displayName: The name of your extension that will be displayed in the UI.
  • permissions: The permissions that your extension needs to run.
    • hosts: The hosts (URLs) that your extension needs to access.

    • paths: The paths that your extension needs to access. You can use environment variables in the path. The environment variable must be wrapped in {}. For example, {ENV_1} will be replaced with the value of ENV_1 environment variable.

      Keys in the paths object is the location in the user's filesystem. Values in the paths object is where the actual directory will be accessible in the extension. Eg.

      "paths": {
        "/test": "/"
      }
      

      To access /test/file.txt in the extension, you can use the path /file.txt.

Prerequisites

Moosync extensions are built and packaged using Bazel.

Installing Bazel

Please follow the official Bazel installation guide to install Bazel (via Bazelisk) on your system:

Verify your installation by running:

bazel --version

Writing your first extension

1. Workspace Setup

In your workspace root, create or update MODULE.bazel to depend on extensions_sdk:

module(
    name = "my_extensions",
    version = "1.0.0",
)

bazel_dep(name = "extensions_sdk", version = "1.0.0")

2. Generating Boilerplate

The easiest way to get started is by using the built-in scaffolding tool. This generates both the Bazel BUILD file and the starter code for your extension:

{{#tabs }} {{#tab name="Rust" }}

bazel run @extensions_sdk//tools:scaffold -- --lang rust --name my_rust_ext

{{#endtab }} {{#tab name="Golang" }}

bazel run @extensions_sdk//tools:scaffold -- --lang go --name my_go_ext

{{#endtab }} {{#tab name="Python" }}

bazel run @extensions_sdk//tools:scaffold -- --lang python --name my_py_ext

{{#endtab }} {{#tab name="Javascript" }}

bazel run @extensions_sdk//tools:scaffold -- --lang js --name my_js_ext

{{#endtab }} {{#endtabs }}


3. Extension Rules & Implementation

{{#tabs }} {{#tab name="Rust" }}

Generated BUILD Rule

The scaffolding tool generates a BUILD file using rust_extension:

load("@extensions_sdk//:defs.bzl", "rust_extension")

package(default_visibility = ["//visibility:public"])

rust_extension(
    name = "rust_sample",
    srcs = ["src/lib.rs"],
    package_name = "moosync.sample.rust",
    display_name = "Sample Extension (Rust)",
    version = "0.0.1",
)

Implementation

Extensions are represented by the Extension trait. You need to register your extension through the init function:

#![allow(unused)]
fn main() {
struct SampleExtension;

impl Extension for SampleExtension {}
impl Accounts for SampleExtension {}
impl DatabaseEvents for SampleExtension {}
impl PlayerEvents for SampleExtension {}
impl PreferenceEvents for SampleExtension {}
impl ContextMenu for SampleExtension {}

#[unsafe(no_mangle)]
pub extern "C" fn init() {
    info!("Initializing SampleExtension");
    register_extension(Box::new(SampleExtension)).unwrap();
    info!("Initialized SampleExtension");
}
}

Build

bazel build //my_rust_ext:my_rust_ext

{{#endtab }} {{#tab name="Golang" }}

Generated BUILD Rule

The scaffolding tool generates a BUILD file using go_extension:

load("@extensions_sdk//:defs.bzl", "go_extension")

package(default_visibility = ["//visibility:public"])

go_extension(
    name = "go_sample",
    srcs = ["main.go"],
    package_name = "moosync.sample.go",
    display_name = "Sample Extension (Go)",
    version = "0.0.1",
)

Implementation

Extensions are represented by embedding api.DefaultExtension. You need to register your extension through the entry function:

type SampleExtension struct {
	api.DefaultExtension
}

//go:wasmexport entry
func entry() {
	extension := &SampleExtension{}
	api.RegisterExtension(extension)
}

func main() {}

Build

bazel build //my_go_ext:my_go_ext

{{#endtab }} {{#tab name="Python" }}

Generated BUILD Rule

The scaffolding tool generates a BUILD file using py_extension:

load("@extensions_sdk//:defs.bzl", "py_extension")

package(default_visibility = ["//visibility:public"])

py_extension(
    name = "py_sample",
    srcs = ["main.py"],
    main = "main.py",
    package_name = "moosync.sample.python",
    display_name = "Sample Extension (Python)",
    version = "0.0.1",
)

Implementation

All extensions must start in a module called main. Extensions are represented by the Extension class. You need to register your extension through the entry function:

class SampleExtension(Extension):
    def __init__(self):
        super().__init__()

def entry():
    print("Initializing SampleExtension")
    register_extension(SampleExtension())

Build

bazel build //my_py_ext:my_py_ext

{{#endtab }} {{#tab name="Javascript" }}

Generated BUILD Rule

The scaffolding tool generates a BUILD file using js_extension:

load("@extensions_sdk//:defs.bzl", "js_extension")

package(default_visibility = ["//visibility:public"])

js_extension(
    name = "js_sample",
    srcs = ["src/index.ts"],
    package_name = "moosync.sample.js",
    display_name = "Sample Extension (JavaScript)",
    version = "0.0.1",
)

Implementation

You need to re-export all methods provided by wasm-extension-js package. The entrypoint of your extension is a function called entry:

export function entry(): number {
  const api = getApi();

  console.log("Initialized sample extension");
  return 0;
}

Build

bazel build //my_js_ext:my_js_ext

{{#endtab }} {{#endtabs }}

Required implementations

All extensions must return ProviderScopes. These scopes determine which events are sent to your extension.

Lets consider the search scope for this example. Adding the search scope would cause the main app to request search results from your extension.

{{#tabs }} {{#tab name="Rust" }}

#![allow(unused)]
fn main() {
impl Provider for SampleExtension {
    fn get_provider_scopes(&self) -> api::MoosyncResult<Vec<ExtensionProviderScope>> {
        Ok(vec![ExtensionProviderScope::Search])
    }

    fn search(&self, req: RequestedSearchResultRequest) -> api::MoosyncResult<SearchResult> {
        info!("Search requested for query: {}", req.query);

        let url = format!("https://api.spotify.com/v1/search?q={}&type=track", req.query);
        let resp = moosync_edk::http::get(&url, None)?;
        if resp.is_success() {
            info!("Search API response: {}", resp.text().unwrap_or_default());
        }

        Ok(SearchResult {
            songs: vec![],
            artists: vec![],
            playlists: vec![],
            albums: vec![],
            genres: vec![],
        })
    }
}
}

{{#endtab }} {{#tab name="Golang" }}

func (s *SampleExtension) GetProviderScopes() ([]extensions.ExtensionProviderScope, error) {
	return []extensions.ExtensionProviderScope{
		extensions.ExtensionProviderScope_SEARCH,
	}, nil
}

func (s *SampleExtension) Search(req *extensions.RequestedSearchResultRequest) (*songs.SearchResult, error) {
	api.LogInfo("Search called with query: %s", req.GetQuery())

	url := "https://api.spotify.com/v1/search?q=" + req.GetQuery() + "&type=track"
	resp, err := api.HttpGet(url, nil)
	if err != nil {
		return nil, err
	}
	if resp.OK() {
		api.LogInfo("Search response: %s", resp.Text())
	}

	return &songs.SearchResult{
		Songs:     []*songs.Song{},
		Playlists: []*songs.Playlist{},
		Artists:   []*songs.Artist{},
		Albums:    []*songs.Album{},
		Genres:    []*songs.Genre{},
	}, nil
}

{{#endtab }} {{#tab name="Python" }}

class SearchProviderExtension(Extension):
    def get_provider_scopes(self, req):
        return extensions_pb2.GetProviderScopesResponse(
            scopes=[extensions_pb2.ExtensionProviderScope.SEARCH]
        )

    def search(self, req):
        print(f"Search query: {req.query}")

        url = f"https://api.spotify.com/v1/search?q={req.query}&type=track"
        resp = http_get(url)
        if resp.ok:
            print(f"Search response: {resp.text}")

        return songs_pb2.SearchResult(
            songs=[],
            artists=[],
            playlists=[],
            albums=[],
            genres=[]
        )

{{#endtab }} {{#tab name="Javascript" }}

export function registerProviderHandlers() {
  const api = getApi();

  api.on("getProviderScopes", () => {
    return new GetProviderScopesResponse({
      scopes: [
        13 as any // ExtensionProviderScope.SEARCH
      ]
    });
  });

  api.on("requestedSearchResult", async (req) => {
    console.log("Search query:", req.query);

    const url = `https://api.spotify.com/v1/search?q=${encodeURIComponent(req.query)}&type=track`;
    const resp = await api.fetch(url);
    if (resp.ok) {
      const text = await resp.text();
      console.log("Search response:", text);
    }

    return new RequestedSearchResultResponse({
      songs: []
    });
  });
}

{{#endtab }} {{#endtabs }}

Using the API

The extension development kit provides APIs to interact with the main app and perform external network operations.

Fetching App State

For this example lets consider the getCurrentSong API. getCurrentSong returns the actively playing song.

{{#tabs }} {{#tab name="Rust" }}

#![allow(unused)]
fn main() {
impl Provider for SampleExtension {
    fn scrobble(&self, _req: ScrobbleRequest) -> api::MoosyncResult<()> {
        let song = get_current_song()?;
        if let Some(song) = song {
            if let Some(inner) = song.song {
                if let Some(title) = inner.title {
                    info!("Currently playing song: {}", title);
                }
            }
        }
        Ok(())
    }
}
}

{{#endtab }} {{#tab name="Golang" }}

func (s *SampleExtension) Scrobble(req *extensions.ScrobbleRequest) error {
	song, err := api.GetCurrentSong()
	if err != nil {
		return err
	}
	if song != nil && song.GetSong() != nil {
		api.LogInfo("Currently playing song: %s", song.GetSong().GetTitle())
	}
	return nil
}

{{#endtab }} {{#tab name="Python" }}

class ScrobbleExtension(Extension):
    def scrobble(self, req):
        song = self.api.get_current_song()
        if song and song.song:
            print(f"Currently playing: {song.song.title}")

{{#endtab }} {{#tab name="Javascript" }}

export function registerApiUsage() {
  const api = getApi();

  api.on("scrobble", async (req) => {
    const song = await api.getCurrentSong();
    if (song && song.song && song.song.title) {
      console.log("Currently playing song:", song.song.title);
    }
    return new ScrobbleResponse();
  });
}

{{#endtab }} {{#endtabs }}


Making HTTP Requests

Extensions can make outgoing HTTP requests through the host runner using the SDK's HTTP APIs. Both single requests and batch/parallel requests are supported.

Permissions: To communicate with external servers, specify allowed hosts in your extension manifest (package.json) under "allowed_hosts" (for example, ["api.spotify.com", "*.last.fm"]).

{{#tabs }} {{#tab name="Rust" }}

#![allow(unused)]
fn main() {
pub fn search_and_fetch_details(query: &str) -> api::MoosyncResult<()> {
    use moosync_edk::http::{self, HttpRequest};

    let url = format!("https://api.spotify.com/v1/search?q={}&type=track", query);
    let resp = http::get(&url, None)?;
    if resp.is_success() {
        info!("Search response: {}", resp.text().unwrap_or_default());
    }

    let post_req = HttpRequest::post("https://api.spotify.com/v1/playlists")
        .header("Authorization", "Bearer token123")
        .body(r#"{"name":"My Playlist"}"#.as_bytes().to_vec())
        .timeout_ms(5000);
    let post_resp = http::request(&post_req)?;
    info!("Create playlist status: {}", post_resp.status_code);

    let track_urls = vec![
        "https://api.spotify.com/v1/tracks/1",
        "https://api.spotify.com/v1/tracks/2",
    ];
    let responses = http::batch_get(&track_urls, None)?;
    for r in responses {
        info!("Track status: {}", r.status_code);
    }
    Ok(())
}
}

{{#endtab }} {{#tab name="Golang" }}

func searchAndFetchDetails(query string) error {
	url := "https://api.spotify.com/v1/search?q=" + query + "&type=track"
	resp, err := api.HttpGet(url, nil)
	if err != nil {
		return err
	}
	if resp.OK() {
		api.LogInfo("Search response: %s", resp.Text())
	}

	postReq := api.HttpRequest{
		URL:       "https://api.spotify.com/v1/playlists",
		Method:    "POST",
		Headers:   map[string]string{"Authorization": "Bearer token123"},
		Body:      []byte(`{"name":"My Playlist"}`),
		TimeoutMs: 5000,
	}
	postResp, err := api.SendHttpRequest(postReq)
	if err != nil {
		return err
	}
	api.LogInfo("Create playlist status: %d", postResp.StatusCode)

	trackURLs := []string{
		"https://api.spotify.com/v1/tracks/1",
		"https://api.spotify.com/v1/tracks/2",
	}
	resps, errs := api.BatchHttpGet(trackURLs, nil)
	if len(errs) > 0 {
		return errs[0]
	}
	for _, r := range resps {
		api.LogInfo("Track status: %d", r.StatusCode)
	}
	return nil
}

{{#endtab }} {{#tab name="Python" }}

from moosync_edk import http_get, http_request, http_batch_get

def search_and_fetch_details(query):
    url = f"https://api.spotify.com/v1/search?q={query}&type=track"
    resp = http_get(url)
    if resp.ok:
        print(f"Search response: {resp.text}")

    post_resp = http_request(
        url="https://api.spotify.com/v1/playlists",
        method="POST",
        headers={"Authorization": "Bearer token123"},
        body='{"name": "My Playlist"}',
        timeout_ms=5000,
    )
    print(f"Create playlist status: {post_resp.status_code}")

    track_urls = [
        "https://api.spotify.com/v1/tracks/1",
        "https://api.spotify.com/v1/tracks/2",
    ]
    resps = http_batch_get(track_urls)
    for r in resps:
        print(f"Track status: {r.status_code}")

{{#endtab }} {{#tab name="Javascript" }}

export async function searchAndFetchDetails(query: string) {
  const api = getApi();

  const url = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track`;
  const resp = await api.fetch(url);
  if (resp.ok) {
    const text = await resp.text();
    console.log("Search response:", text);
  }

  const postResp = await api.fetch({
    url: "https://api.spotify.com/v1/playlists",
    method: "POST",
    headers: { Authorization: "Bearer token123" },
    body: JSON.stringify({ name: "My Playlist" }),
    timeoutMs: 5000,
  });
  console.log("Create playlist status:", postResp.status);

  const responses = await api.batchFetch([
    "https://api.spotify.com/v1/tracks/1",
    "https://api.spotify.com/v1/tracks/2",
  ]);
  for (const r of responses) {
    console.log("Track status:", r.status);
  }
}

{{#endtab }} {{#endtabs }}

Full extension example

Here is a complete extension example combining registration, provider capabilities, and API usage:

{{#tabs }} {{#tab name="Rust" }}

#![allow(unused)]
#![no_main]

fn main() {
use moosync_edk::{
    ExtensionProviderScope, RequestedSearchResultRequest, ScrobbleRequest, SearchResult,
    api::{
        self, Accounts, ContextMenu, DatabaseEvents, Extension, PlayerEvents, PreferenceEvents,
        Provider, extension_api::get_current_song,
    },
    handler::register_extension,
    info,
};

// ANCHOR: first_extension
struct SampleExtension;

impl Extension for SampleExtension {}
impl Accounts for SampleExtension {}
impl DatabaseEvents for SampleExtension {}
impl PlayerEvents for SampleExtension {}
impl PreferenceEvents for SampleExtension {}
impl ContextMenu for SampleExtension {}

#[unsafe(no_mangle)]
pub extern "C" fn init() {
    info!("Initializing SampleExtension");
    register_extension(Box::new(SampleExtension)).unwrap();
    info!("Initialized SampleExtension");
}
// ANCHOR_END: first_extension

impl Provider for SampleExtension {
// ANCHOR: provider
    fn get_provider_scopes(&self) -> api::MoosyncResult<Vec<ExtensionProviderScope>> {
        Ok(vec![ExtensionProviderScope::Search])
    }

    fn search(&self, req: RequestedSearchResultRequest) -> api::MoosyncResult<SearchResult> {
        info!("Search requested for query: {}", req.query);

        let url = format!("https://api.spotify.com/v1/search?q={}&type=track", req.query);
        let resp = moosync_edk::http::get(&url, None)?;
        if resp.is_success() {
            info!("Search API response: {}", resp.text().unwrap_or_default());
        }

        Ok(SearchResult {
            songs: vec![],
            artists: vec![],
            playlists: vec![],
            albums: vec![],
            genres: vec![],
        })
    }
// ANCHOR_END: provider

// ANCHOR: api_usage
    fn scrobble(&self, _req: ScrobbleRequest) -> api::MoosyncResult<()> {
        let song = get_current_song()?;
        if let Some(song) = song {
            if let Some(inner) = song.song {
                if let Some(title) = inner.title {
                    info!("Currently playing song: {}", title);
                }
            }
        }
        Ok(())
    }
// ANCHOR_END: api_usage
}

// ANCHOR: http_usage
pub fn search_and_fetch_details(query: &str) -> api::MoosyncResult<()> {
    use moosync_edk::http::{self, HttpRequest};

    let url = format!("https://api.spotify.com/v1/search?q={}&type=track", query);
    let resp = http::get(&url, None)?;
    if resp.is_success() {
        info!("Search response: {}", resp.text().unwrap_or_default());
    }

    let post_req = HttpRequest::post("https://api.spotify.com/v1/playlists")
        .header("Authorization", "Bearer token123")
        .body(r#"{"name":"My Playlist"}"#.as_bytes().to_vec())
        .timeout_ms(5000);
    let post_resp = http::request(&post_req)?;
    info!("Create playlist status: {}", post_resp.status_code);

    let track_urls = vec![
        "https://api.spotify.com/v1/tracks/1",
        "https://api.spotify.com/v1/tracks/2",
    ];
    let responses = http::batch_get(&track_urls, None)?;
    for r in responses {
        info!("Track status: {}", r.status_code);
    }
    Ok(())
}
// ANCHOR_END: http_usage
}

{{#endtab }} {{#tab name="Golang" }}

package main

import (
	extensions "github.com/moosync/moosync/types/extensions"
	songs "github.com/moosync/moosync/types/songs"

	"github.com/Moosync/extensions-sdk/wasm-extension-go/pkg/api"
)

// ANCHOR: first_extension
type SampleExtension struct {
	api.DefaultExtension
}

//go:wasmexport entry
func entry() {
	extension := &SampleExtension{}
	api.RegisterExtension(extension)
}

func main() {}
// ANCHOR_END: first_extension

// ANCHOR: provider
func (s *SampleExtension) GetProviderScopes() ([]extensions.ExtensionProviderScope, error) {
	return []extensions.ExtensionProviderScope{
		extensions.ExtensionProviderScope_SEARCH,
	}, nil
}

func (s *SampleExtension) Search(req *extensions.RequestedSearchResultRequest) (*songs.SearchResult, error) {
	api.LogInfo("Search called with query: %s", req.GetQuery())

	url := "https://api.spotify.com/v1/search?q=" + req.GetQuery() + "&type=track"
	resp, err := api.HttpGet(url, nil)
	if err != nil {
		return nil, err
	}
	if resp.OK() {
		api.LogInfo("Search response: %s", resp.Text())
	}

	return &songs.SearchResult{
		Songs:     []*songs.Song{},
		Playlists: []*songs.Playlist{},
		Artists:   []*songs.Artist{},
		Albums:    []*songs.Album{},
		Genres:    []*songs.Genre{},
	}, nil
}
// ANCHOR_END: provider

// ANCHOR: api_usage
func (s *SampleExtension) Scrobble(req *extensions.ScrobbleRequest) error {
	song, err := api.GetCurrentSong()
	if err != nil {
		return err
	}
	if song != nil && song.GetSong() != nil {
		api.LogInfo("Currently playing song: %s", song.GetSong().GetTitle())
	}
	return nil
}
// ANCHOR_END: api_usage

// ANCHOR: http_usage
func searchAndFetchDetails(query string) error {
	url := "https://api.spotify.com/v1/search?q=" + query + "&type=track"
	resp, err := api.HttpGet(url, nil)
	if err != nil {
		return err
	}
	if resp.OK() {
		api.LogInfo("Search response: %s", resp.Text())
	}

	postReq := api.HttpRequest{
		URL:       "https://api.spotify.com/v1/playlists",
		Method:    "POST",
		Headers:   map[string]string{"Authorization": "Bearer token123"},
		Body:      []byte(`{"name":"My Playlist"}`),
		TimeoutMs: 5000,
	}
	postResp, err := api.SendHttpRequest(postReq)
	if err != nil {
		return err
	}
	api.LogInfo("Create playlist status: %d", postResp.StatusCode)

	trackURLs := []string{
		"https://api.spotify.com/v1/tracks/1",
		"https://api.spotify.com/v1/tracks/2",
	}
	resps, errs := api.BatchHttpGet(trackURLs, nil)
	if len(errs) > 0 {
		return errs[0]
	}
	for _, r := range resps {
		api.LogInfo("Track status: %d", r.StatusCode)
	}
	return nil
}
// ANCHOR_END: http_usage

{{#endtab }} {{#tab name="Python" }}

from moosync_edk import Extension, register_extension
from core.types.protos import extensions_pb2, songs_pb2

# ANCHOR: first_extension
class SampleExtension(Extension):
    def __init__(self):
        super().__init__()

def entry():
    print("Initializing SampleExtension")
    register_extension(SampleExtension())
# ANCHOR_END: first_extension

# ANCHOR: provider
class SearchProviderExtension(Extension):
    def get_provider_scopes(self, req):
        return extensions_pb2.GetProviderScopesResponse(
            scopes=[extensions_pb2.ExtensionProviderScope.SEARCH]
        )

    def search(self, req):
        print(f"Search query: {req.query}")

        url = f"https://api.spotify.com/v1/search?q={req.query}&type=track"
        resp = http_get(url)
        if resp.ok:
            print(f"Search response: {resp.text}")

        return songs_pb2.SearchResult(
            songs=[],
            artists=[],
            playlists=[],
            albums=[],
            genres=[]
        )
# ANCHOR_END: provider

# ANCHOR: api_usage
class ScrobbleExtension(Extension):
    def scrobble(self, req):
        song = self.api.get_current_song()
        if song and song.song:
            print(f"Currently playing: {song.song.title}")
# ANCHOR_END: api_usage

# ANCHOR: http_usage
from moosync_edk import http_get, http_request, http_batch_get

def search_and_fetch_details(query):
    url = f"https://api.spotify.com/v1/search?q={query}&type=track"
    resp = http_get(url)
    if resp.ok:
        print(f"Search response: {resp.text}")

    post_resp = http_request(
        url="https://api.spotify.com/v1/playlists",
        method="POST",
        headers={"Authorization": "Bearer token123"},
        body='{"name": "My Playlist"}',
        timeout_ms=5000,
    )
    print(f"Create playlist status: {post_resp.status_code}")

    track_urls = [
        "https://api.spotify.com/v1/tracks/1",
        "https://api.spotify.com/v1/tracks/2",
    ]
    resps = http_batch_get(track_urls)
    for r in resps:
        print(f"Track status: {r.status_code}")
# ANCHOR_END: http_usage

{{#endtab }} {{#tab name="Javascript" }}

import {
  GetProviderScopesResponse,
  RequestedSearchResultResponse,
  ScrobbleResponse,
  getApi,
} from "wasm-extension-js";

export { handle_extension_command } from "wasm-extension-js";

// ANCHOR: first_extension
export function entry(): number {
  const api = getApi();

  console.log("Initialized sample extension");
  return 0;
}
// ANCHOR_END: first_extension

// ANCHOR: provider
export function registerProviderHandlers() {
  const api = getApi();

  api.on("getProviderScopes", () => {
    return new GetProviderScopesResponse({
      scopes: [
        13 as any // ExtensionProviderScope.SEARCH
      ]
    });
  });

  api.on("requestedSearchResult", async (req) => {
    console.log("Search query:", req.query);

    const url = `https://api.spotify.com/v1/search?q=${encodeURIComponent(req.query)}&type=track`;
    const resp = await api.fetch(url);
    if (resp.ok) {
      const text = await resp.text();
      console.log("Search response:", text);
    }

    return new RequestedSearchResultResponse({
      songs: []
    });
  });
}
// ANCHOR_END: provider

// ANCHOR: api_usage
export function registerApiUsage() {
  const api = getApi();

  api.on("scrobble", async (req) => {
    const song = await api.getCurrentSong();
    if (song && song.song && song.song.title) {
      console.log("Currently playing song:", song.song.title);
    }
    return new ScrobbleResponse();
  });
}
// ANCHOR_END: api_usage

// ANCHOR: http_usage
export async function searchAndFetchDetails(query: string) {
  const api = getApi();

  const url = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track`;
  const resp = await api.fetch(url);
  if (resp.ok) {
    const text = await resp.text();
    console.log("Search response:", text);
  }

  const postResp = await api.fetch({
    url: "https://api.spotify.com/v1/playlists",
    method: "POST",
    headers: { Authorization: "Bearer token123" },
    body: JSON.stringify({ name: "My Playlist" }),
    timeoutMs: 5000,
  });
  console.log("Create playlist status:", postResp.status);

  const responses = await api.batchFetch([
    "https://api.spotify.com/v1/tracks/1",
    "https://api.spotify.com/v1/tracks/2",
  ]);
  for (const r of responses) {
    console.log("Track status:", r.status);
  }
}
// ANCHOR_END: http_usage

{{#endtab }} {{#endtabs }}

Language API Documentation

API reference documentation for each supported language: