OpenDeezer
Go library · open source · local-first

OpenDeezer SDK

Build on the OpenDeezer engine — the Deezer API, track decode/download, OpenDeezer Connect, and remote control, as a Go library.

go get github.com/Cycl0o0/OpenDeezer

Needs a Deezer Premium account. AGPL-3.0.

Packages

Four packages, one module. Mix and match: API-only tools skip sdk/player; non-Go clients can call the HTTP control API directly without importing anything.

Package Purpose
github.com/Cycl0o0/OpenDeezer/sdk/deezer Deezer API client + track decode/download
github.com/Cycl0o0/OpenDeezer/sdk/connect OpenDeezer Connect — LAN discovery, drive or host a device
github.com/Cycl0o0/OpenDeezer/sdk/control Control server + client (HTTP/JSON API + phone web remote)
github.com/Cycl0o0/OpenDeezer/sdk/player In-process audio playback (requires cgo)

Full Go docs: pkg.go.dev/github.com/Cycl0o0/OpenDeezer/sdk

Authentication (ARL)

Deezer uses a long-lived cookie called the ARL (Audio Reference Link) for authentication. Get yours from a browser session: open deezer.com, press F12, go to Application → Cookies → arl.

import dz "github.com/Cycl0o0/OpenDeezer/sdk/deezer"

client := dz.New(os.Getenv("DEEZER_ARL"))
if err := client.Login(); err != nil {
    // errors.Is(err, dz.ErrARLExpired) → ARL needs refreshing
    log.Fatal(err)
}
acc := client.Account()
fmt.Printf("Hello %s (%s)\n", acc.Name, acc.Offer)

The ARL only leaves your machine as HTTPS requests to deezer.com and media.deezer.com. Treat it like a password — it grants full access to your account.

Browse

Search, global charts, your library, artist profiles and synced lyrics — all from the same client.

// Search
results, _ := client.Search("Radiohead")
for _, t := range results.Tracks {
    fmt.Println(t.Name, "—", t.ArtistLine())
}

// Charts (global top 50)
charts, _ := client.Charts("0")

// Favorites / playlists (requires login)
tracks, _    := client.Favorites()
playlists, _ := client.Playlists()

// Artist profile
page, _ := client.ArtistProfile("27")   // Radiohead
fmt.Println(page.Artist.Name, page.Artist.NbFans, "fans")

// Lyrics
lyr, _ := client.Lyrics(trackID)
if lyr.IsSynced() {
    for _, line := range lyr.Synced {
        fmt.Printf("%d ms  %s\n", line.TimeMS, line.Text)
    }
}

Download & decode a track

PrepareStream resolves the CDN URL and decryption key. DownloadTrack fetches, Blowfish-decrypts (BF_CBC_STRIPE), and writes the audio bytes to any io.Writer.

import (
    "os"
    dz "github.com/Cycl0o0/OpenDeezer/sdk/deezer"
)

client.SetQuality(dz.QualityHigh) // prefer MP3 320; falls back if not entitled

plan, err := client.PrepareStream(trackID)
if err != nil {
    log.Fatal(err)
}
fmt.Println("Format:", dz.FormatLabel(plan.Format)) // "MP3 · 320 kbps"

f, _ := os.Create("track.mp3")
defer f.Close()
if err := dz.DownloadTrack(plan, f); err != nil {
    log.Fatal(err)
}

Quality levels

ConstantFormatRequires
dz.QualityNormal MP3 128 kbps Any account
dz.QualityHigh MP3 320 kbps Premium
dz.QualityLossless FLAC HiFi

Deezer falls back to the highest quality the account is entitled to automatically.

Decrypt an in-memory buffer

plain, err := dz.DecryptBytes(trackID, encryptedBytes)

OpenDeezer Connect

LAN discovery and device handoff, like Spotify Connect. The SDK is symmetric: you can drive another device (out) or be driven by one (in). Both sides cover the same transport command set: play/pause, next, prev, stop, restart, seek, volume, repeat, shuffle, play-track, play-playlist.

DirectionWhatAPI
out Discover + control other devices connect.Discover, connect.RemoteClient
in Be discoverable + controllable connect.Host (or connect.Advertise for discovery only)

Out — discover and drive a device

import "github.com/Cycl0o0/OpenDeezer/sdk/connect"

// Find devices (2-second probe window).
devices, _ := connect.Discover(2*time.Second, 0)
for _, d := range devices {
    fmt.Printf("%s at %s\n", d.Name, d.Addr)
}

// Drive the first device (same-account auth — no token needed).
rc := connect.NewRemoteClient(devices[0].Addr, "", myUserID)
st, _ := rc.PlayPause()
fmt.Println("state:", st.State)

In — be a controllable device

connect.Host ties a control endpoint together with LAN advertising. It accepts the same command set a RemoteClient sends.

host := connect.NewHost(
    connect.HostConfig{
        Control: connect.Config{Addr: ":7654", SameAccountOnly: true},
        Name:    "My Player", Client: "myapp", Version: "1.0",
    },
    func() connect.State { return currentState() },
    func() connect.Account {
        a := client.Account()
        return connect.Account{UserID: a.UserID, Name: a.Name, Offer: a.Offer}
    },
    connect.Commands{
        PlayPause: player.TogglePause,
        Stop:      player.Stop,
        SetVolume: player.SetVolume,
        PlayTrack: playByID,
    },
    client, // browse routes; nil to disable
)
host.Start()
defer host.Close()
// host.Server().EnablePairing() to also accept the phone web remote.

If you already run your own control server, advertise the discovery half alone:

resp, _ := connect.Advertise(func() connect.AdvertiseInfo {
    return connect.AdvertiseInfo{Name: "My Player", Client: "myapp", Version: "1.0"}
}, controlPort)
defer resp.Close()

Auth modes for RemoteClient

Check Whoami.Auth on the target device to know which credential to supply:

Auth valueWhat to pass
"token" Pass token="<bearer-token>"
"account" Pass accountID="<your Deezer user id>"
"session" Use control.NewClient and pair via GET /remote
"none" Empty strings

Remote control API

The control API is also symmetric: control.Server hosts a controllable endpoint (in), control.Client drives one (out). Non-Go clients can hit the same HTTP endpoints directly — this is the “and more”: any language, any HTTP client.

Enable the server

export OPENDEEZER_CONTROL=1        # localhost only (127.0.0.1:7654)
export OPENDEEZER_CONTROL=:7654    # bind all interfaces (LAN remote)
# or:
echo 1 > ~/.config/opendeezer/control.txt

Control server (in)

Host a controllable endpoint that phones, AI agents, or other OpenDeezer clients can drive.

import "github.com/Cycl0o0/OpenDeezer/sdk/control"

srv := control.NewServer(
    control.Config{
        Addr:  ":7654",
        Token: "my-secret-token",
    },
    func() control.State { return currentState() },
    func() control.Account {
        a := client.Account()
        return control.Account{UserID: a.UserID, Name: a.Name, Offer: a.Offer}
    },
    control.Commands{
        PlayPause: player.TogglePause,
        Next:      queue.Next,
        Stop:      player.Stop,
        Seek:      player.SeekMS,
        SetVolume: player.SetVolume,
    },
    client, // for GET /search and GET /playlists; nil to disable
)
srv.SetVersion("1.0")
srv.Start()
defer srv.Close()

Phone web remote

Enable pairing to let a phone control playback via a browser at http://<host>:7654/remote. Display the 6-digit code to the user; they enter it in the browser UI.

srv := control.NewServer(
    control.Config{Addr: ":7654", WebRemote: true},
    // ... same state/account/commands as above
)
srv.Start()
code := srv.EnablePairing() // display this 6-digit code to the user
fmt.Printf("Open http://192.168.1.X:7654/remote — code: %s\n", code)

Control client (out)

c := control.NewClient("http://192.168.1.5:7654", "my-secret-token", "")
st, _ := c.Status()
fmt.Println(st.State, st.Track.Title)
c.SetVolume(0.8)
c.SeekMS(30000)
c.PlayTrack("3135556")

HTTP endpoint reference

The API speaks plain HTTP/JSON, so any language can use it. Reads are GET, mutations are POST. Mutations reject a browser Origin header (already the case for native clients and curl).

MethodPathAction
GET/whoamiAccount name + auth mode (unauthenticated)
GET/statusPlayback snapshot: state, track, position, volume, queue
GET/playlistsUser’s playlist list
GET/search?q=Search tracks
POST/playpauseToggle play / pause
POST/nextSkip to next track
POST/prevPrevious track
POST/stopStop playback
POST/restartRestart current track from the beginning
POST/seek?ms=Seek to position in milliseconds
POST/volume?v=Set volume (0.0–1.0)
POST/repeatCycle repeat mode
POST/shuffleToggle shuffle
POST/play/track?id=Play a track by Deezer track id
POST/play/playlist?id=Play a playlist by id
GET/remotePhone web remote UI (requires WebRemote: true)
POST/pair?code=Pair a phone web remote session with the 6-digit code

Auth

Credentials go in request headers only:
• X-OpenDeezer-Token: <token> — strongest; set via OPENDEEZER_CONTROL_TOKEN.
• X-OpenDeezer-Account: <deezer-user-id> — same-account (LAN-trust); default when bound to a non-loopback address with no token.
• No header — open on localhost only. A LAN bind always requires one of the two — the server refuses to start unauthenticated on a non-loopback address.

curl examples

# Playback status (same-account auth)
curl http://192.168.1.5:7654/status \
  -H "X-OpenDeezer-Account: <your_user_id>"

# Toggle play/pause (token auth)
curl -X POST http://192.168.1.5:7654/playpause \
  -H "X-OpenDeezer-Token: my-secret-token"

# Seek to 30 seconds
curl -X POST "http://192.168.1.5:7654/seek?ms=30000" \
  -H "X-OpenDeezer-Token: my-secret-token"

# Set volume to 80%
curl -X POST "http://192.168.1.5:7654/volume?v=0.8" \
  -H "X-OpenDeezer-Token: my-secret-token"

# Play a track by id
curl -X POST "http://192.168.1.5:7654/play/track?id=3135556" \
  -H "X-OpenDeezer-Token: my-secret-token"

# Search
curl "http://192.168.1.5:7654/search?q=daft+punk" \
  -H "X-OpenDeezer-Token: my-secret-token"

In-process playback

The sdk/player package wraps the audio engine (miniaudio via malgo, or oto, selected by build tag). It requires cgo. Omit this import if you only need API access, search, or download/decrypt — the other three packages are pure Go.

import (
    "github.com/Cycl0o0/OpenDeezer/sdk/player"
    dz "github.com/Cycl0o0/OpenDeezer/sdk/deezer"
)

p, _ := player.NewPlayer()
defer p.Close()

p.SetReplayGain(true)
p.SetVolume(0.9)

// Play a track.
plan, _ := client.PrepareStream(trackID)
p.Play(plan, track.DurationMS)

// Advance queue when track ends.
p.SetOnFinish(func() { /* load next */ })

// Gapless: preload the next track before this one ends.
nextPlan, _ := client.PrepareStream(nextTrackID)
p.Preload(nextPlan, nextTrack.DurationMS)

Examples

Runnable examples live in examples/ in the repo. Each one is a standalone main package.

DirectoryDirectionWhat it shows
examples/search Login + search + print results
examples/download Login → PrepareStream → DownloadTrack → file
examples/connect out Discover LAN devices + send PlayPause
examples/host in Be a discoverable, controllable Connect device
examples/remote-server in Host a control server + poll it via the client
DEEZER_ARL=<your_arl> go run ./examples/search "Daft Punk"
DEEZER_ARL=<your_arl> go run ./examples/download 3135556
DEEZER_ARL=<your_arl> go run ./examples/connect
DEEZER_ARL=<your_arl> go run ./examples/host
DEEZER_ARL=<your_arl> go run ./examples/remote-server