control

package
v3.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package control exposes playback control + status over a small HTTP/JSON API. It is the shared foundation for remote control (one OpenDeezer client driving another) and the MCP server (an AI agent driving playback). A frontend wires it like the MPRIS bridge: provide a status snapshot func + a set of command callbacks, plus the Deezer client for read-only browse (search/playlists).

Auth has three modes, picked by Config. Credentials are accepted via request HEADERS only (never the query string, which leaks into logs/history), except that GET /events alone accepts ?session= for the browser EventSource API, which cannot set X-OpenDeezer-Session:

  • Token: a bearer token in "X-OpenDeezer-Token". Strongest.
  • Same-account: no token, but a controller must prove it is logged into the SAME Deezer account by sending its OWN Deezer user id in "X-OpenDeezer-Account". This is accepted directly from loopback clients (backward compat) and from LAN peers (for token-less same-account Connect and the LAN-trust tradeoff); the plaintext id is accepted on non-loopback binds. Pairing via short-TTL single-use 6-digit code may still be used to obtain a per-device session token (X-OpenDeezer-Session) instead.
  • Session (web remote): a phone pairs with a 6-digit code minted at enable time; on success it receives a short-lived session token sent as X-OpenDeezer-Session. The code is now single-use + time-limited; failed verifies have lockout. Per-device ids allow individual revocation.
  • None: open (only safe bound to localhost).

Mutating endpoints require POST and reject requests carrying a browser Origin header, so a web page the user happens to visit can't drive playback (CSRF). The exception is requests that also carry a valid X-OpenDeezer-Session token: those come from our own SPA (same origin in the browser), so they are allowed. GET /whoami is unauthenticated so a controller can discover the account NAME (not id), auth mode and supported command families before connecting.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Account

type Account struct {
	UserID string
	Name   string
	Offer  string
}

Account is the controlled client's Deezer identity, supplied by a snapshot provider so the HTTP goroutine never reads the deezer.Client's login fields directly (those are written by Login on another goroutine).

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to a control Server over HTTP. It is the shared driver for the MCP server and the TUI's remote-play feature: both point it at another OpenDeezer client's control API and issue the same commands a local user would.

func NewClient

func NewClient(base, token, account string) *Client

NewClient builds a control client. base is the server's URL; token/account are the credentials (send whichever the server requires; empty ones are omitted).

func (*Client) CancelSleepTimer

func (c *Client) CancelSleepTimer() (State, error)

CancelSleepTimer disarms the peer's sleep timer.

func (*Client) CycleRepeat

func (c *Client) CycleRepeat() (State, error)

func (*Client) EQ

func (c *Client) EQ() (EQState, error)

EQ returns the peer's equalizer snapshot (enabled, mono downmix, preamp, per-band gains, active preset and the preset/band lists).

func (*Client) Events

func (c *Client) Events(ctx context.Context) (<-chan State, error)

Events is the backward-compatible state-only view of EventsTyped. Finished events are ignored; state snapshots retain the historical latest-wins buffering behaviour for slow callers.

func (*Client) EventsTyped

func (c *Client) EventsTyped(ctx context.Context) (<-chan Event, error)

EventsTyped subscribes to all recognized peer events. Events retain stream order and include both complete State snapshots and natural-finish edges. Callers should drain the channel promptly; backpressure affects only this connection. The channel closes when ctx is cancelled or the peer disconnects.

func (*Client) HistoryRecent

func (c *Client) HistoryRecent(n int) (json.RawMessage, error)

HistoryRecent returns the peer's recent-listening history as raw JSON.

func (*Client) Next

func (c *Client) Next() (State, error)

func (*Client) PlayAlbum

func (c *Client) PlayAlbum(id string) (State, error)

func (*Client) PlayMixArtist

func (c *Client) PlayMixArtist(id string) (State, error)

func (*Client) PlayMixTrack

func (c *Client) PlayMixTrack(id string) (State, error)

func (*Client) PlayPause

func (c *Client) PlayPause() (State, error)

PlayPause toggles play/pause on the peer.

func (*Client) PlayPlaylist

func (c *Client) PlayPlaylist(id string) (State, error)

func (*Client) PlayTrack

func (c *Client) PlayTrack(id string) (State, error)

func (*Client) Playlists

func (c *Client) Playlists() (json.RawMessage, error)

Playlists returns the raw playlists JSON from the server.

func (*Client) Prev

func (c *Client) Prev() (State, error)

func (*Client) QueueAdd

func (c *Client) QueueAdd(id string, next bool) (State, error)

QueueAdd appends a track to the peer's queue, or inserts it immediately after the current track when next is true.

func (*Client) QueueJump

func (c *Client) QueueJump(index int) (State, error)

func (*Client) QueueMove

func (c *Client) QueueMove(from, to int) (State, error)

func (*Client) QueueRemove

func (c *Client) QueueRemove(index int) (State, error)

func (*Client) Restart

func (c *Client) Restart() (State, error)

func (*Client) Search

func (c *Client) Search(q string) (json.RawMessage, error)

Search returns the raw search-results JSON from the server.

func (*Client) Seek

func (c *Client) Seek(ms int64) (State, error)

func (*Client) SetEQ

func (c *Client) SetEQ(params url.Values) (EQState, error)

SetEQ applies a partial equalizer update. params carries any of the POST /eq query params — on=0|1, mono=0|1, preset=name, band=N&db=X, preamp=dB — and the server applies whichever are present (at least one is required).

func (*Client) SetQueue

func (c *Client) SetQueue(tracksJSON string, index int) (State, error)

SetQueue atomically replaces the peer's host-owned queue. tracksJSON must be a bridge.Track JSON array. index identifies its current row, or is -1 when clearing the queue with an empty array.

func (*Client) SetRepeat

func (c *Client) SetRepeat(mode string) (State, error)

SetRepeat sets the repeat mode on the peer to mode ("off", "all", or "one").

func (*Client) SetShuffle

func (c *Client) SetShuffle(on bool) (State, error)

SetShuffle sets shuffle on (true) or off (false) on the peer.

func (*Client) SetSleepTimer

func (c *Client) SetSleepTimer(minutes int, endOfTrack bool) (State, error)

SetSleepTimer arms the peer's sleep timer: pause after `minutes` (with a fade-out), or when the current track ends if endOfTrack is true.

func (*Client) SetVolume

func (c *Client) SetVolume(v float64) (State, error)

func (*Client) Status

func (c *Client) Status() (State, error)

Status returns the current playback snapshot.

func (*Client) Stop

func (c *Client) Stop() (State, error)

func (*Client) ToggleShuffle

func (c *Client) ToggleShuffle() (State, error)

func (*Client) Whoami

func (c *Client) Whoami() (Whoami, error)

Whoami fetches the server's identity, auth mode and supported command families.

type Commands

type Commands struct {
	PlayPause     func()
	Next          func()
	Prev          func()
	Stop          func()
	Restart       func() // seek to 0
	CycleRepeat   func()
	ToggleShuffle func()
	SetRepeat     func(mode string) // mode: "off"|"all"|"one" (SET variant)
	SetShuffle    func(on bool)     // on: true/false (SET variant)
	Seek          func(ms int64)
	SetVolume     func(v float64)
	PlayTrack     func(id string)
	PlayPlaylist  func(id string)

	// Optional extended-control callbacks return an error for a well-formed
	// request that cannot be applied to the current queue/player state. The HTTP
	// handlers expose those failures as 409 Conflict; nil means unsupported.
	QueueAdd      func(id string, next bool) error
	SetQueue      func(tracksJSON string, index int) error
	QueueJump     func(index int) error
	QueueRemove   func(index int) error
	QueueMove     func(from, to int) error
	PlayAlbum     func(id string) error
	PlayMixTrack  func(id string) error
	PlayMixArtist func(id string) error
	HistoryRecent func(n int) (json.RawMessage, error)

	SetSleepTimer    func(minutes int, endOfTrack bool) // arm the sleep timer
	CancelSleepTimer func()                             // disarm it
}

Commands are the control callbacks a controller exposes (each may be nil).

type Config

type Config struct {
	Addr            string // host:port ("127.0.0.1:7654" localhost, ":7654" LAN)
	Token           string // bearer token; "" disables token auth
	SameAccountOnly bool   // when Token=="", require a matching Deezer account id
	WebRemote       bool   // allow LAN bind with session (pairing) as the sole auth
}

Config configures the control server.

type EQ

type EQ struct {
	State      func() EQState
	SetEnabled func(on bool)
	SetMono    func(on bool)
	SetPreamp  func(db float64)
	SetPreset  func(name string) error
	SetBand    func(band int, db float64) error
}

EQ is the optional equalizer bridge; when nil the /eq endpoints 404. Hosts wire the funcs to the audio player (each may be nil individually).

func PlayerEQ

func PlayerEQ(get func() EQController, presets []string) *EQ

PlayerEQ builds the /eq bridge from a live player getter (get may return nil while the engine is starting) and the core's preset-name list.

type EQController

type EQController interface {
	EQEnabled() bool
	MonoDownmix() bool
	EQPreampDB() float64
	EQGains() []float64
	EQPreset() string
	EQBands() []float64
	SetEQEnabled(on bool)
	SetMonoDownmix(on bool)
	SetEQPreamp(db float64)
	SetEQPreset(name string) error
	SetEQGain(band int, db float64) error
}

EQController is the player subset the /eq endpoint drives; *audio.Player satisfies it (declared here so this package needn't import the engine).

type EQState

type EQState struct {
	Enabled  bool      `json:"enabled"`
	Mono     bool      `json:"mono"`
	PreampDB float64   `json:"preampDb"`
	GainsDB  []float64 `json:"gainsDb"`
	Preset   string    `json:"preset"`
	Bands    []float64 `json:"bands"`
	Presets  []string  `json:"presets"`
}

EQState is the equalizer snapshot returned by GET /eq (same wire shape as corelib's DZEQJSON / odmobile's EQJSON, so every controller renders one UI).

type Event

type Event struct {
	Type     EventType
	State    *State
	Finished *FinishedEvent
}

Event is a tagged item from EventsTyped. Exactly one payload pointer is set for the event types currently defined by the control protocol.

type EventType

type EventType string

EventType identifies a named control SSE event.

const (
	EventTypeState    EventType = "state"
	EventTypeFinished EventType = "finished"
)

type FinishedEvent

type FinishedEvent struct {
	TrackID string `json:"trackId"`
}

FinishedEvent is the payload of an SSE "finished" event. Hosts emit it for natural end-of-track only; decode/stream failures and explicit stops are not finishes.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server serves the control API.

func New

func New(cfg Config, status func() State, account func() Account, cmds Commands, client *deezer.Client) *Server

New builds a control server from cfg. status + account are snapshot providers (called from the HTTP goroutine, so they must be race-free reads); client supplies the browse endpoints (search/playlists).

func (*Server) Addr

func (s *Server) Addr() string

Addr returns the actual listen address (valid after Start).

func (*Server) Close

func (s *Server) Close()

Close stops the server.

func (*Server) DisablePairing

func (s *Server) DisablePairing()

DisablePairing clears the pairing code so no new phones can pair and revokes all existing session tokens (individual sessions can be revoked via their per-device id). It also disables the web remote SPA.

func (*Server) EnablePairing

func (s *Server) EnablePairing() string

EnablePairing mints a fresh 6-digit code, activates pairing, and returns the code. Safe to call multiple times; each call resets the code. The code is single-use and short-TTL (5 minutes).

func (*Server) NotifyFinished

func (s *Server) NotifyFinished(trackID string)

NotifyFinished publishes a natural end-of-track edge to current /events subscribers. It is non-blocking so it is safe to call directly from a player's onFinish callback. Hosts that do not wire this method retain the previous behaviour: controllers can infer finishes from State transitions.

func (*Server) NotifyStateChanged

func (s *Server) NotifyStateChanged()

NotifyStateChanged asks the event loop to publish a fresh snapshot. Calls are deliberately coalesced: the payload is state, not an edge-triggered event, so one pending notification is enough to deliver the newest snapshot.

func (*Server) PairingActive

func (s *Server) PairingActive() bool

PairingActive reports whether pairing is currently enabled.

func (*Server) PairingCode

func (s *Server) PairingCode() string

PairingCode returns the current 6-digit code (empty when not active).

func (*Server) RevokeSession

func (s *Server) RevokeSession(idOrToken string)

RevokeSession revokes the session by its public identifier (returned as "id" from successful /pair) or by its token. Used for per-device revocation and by account-switch / DisablePairing logic.

func (*Server) SetClientInfo

func (s *Server) SetClientInfo(client, device string)

SetClientInfo records the client/platform id + device label for /whoami.

func (*Server) SetEQ

func (s *Server) SetEQ(eq *EQ)

SetEQ wires the equalizer bridge (call before Start).

func (*Server) SetVersion

func (s *Server) SetVersion(v string)

SetVersion records the app version reported by /whoami.

func (*Server) Start

func (s *Server) Start() error

Start binds the port and serves in a background goroutine.

type State

type State struct {
	State      string  `json:"state"` // playing | paused | stopped | loading | error
	Track      *Track  `json:"track,omitempty"`
	PositionMS int64   `json:"positionMs"`
	DurationMS int64   `json:"durationMs"`
	Volume     float64 `json:"volume"` // 0..1
	Repeat     string  `json:"repeat"` // off | all | one
	Shuffle    bool    `json:"shuffle"`
	Format     string  `json:"format,omitempty"`
	Queue      []Track `json:"queue,omitempty"`

	// Sleep timer (0/false when not armed).
	SleepActive      bool  `json:"sleepActive,omitempty"`
	SleepEndOfTrack  bool  `json:"sleepEndOfTrack,omitempty"`
	SleepRemainingMS int64 `json:"sleepRemainingMs,omitempty"`
}

State is the playback snapshot returned by GET /status.

type Track

type Track struct {
	ID         string `json:"id"`
	Title      string `json:"title"`
	Artist     string `json:"artist"`
	ArtistID   string `json:"artistId,omitempty"`
	Album      string `json:"album"`
	Explicit   bool   `json:"explicit"`
	DurationMS int64  `json:"durationMs"`
	ArtworkURL string `json:"artworkUrl,omitempty"`
}

Track is a now-playing / queue entry in the API.

type Whoami

type Whoami struct {
	Name     string   `json:"name"`
	Offer    string   `json:"offer,omitempty"`
	Auth     string   `json:"auth"`              // token | account | session | none
	Version  string   `json:"version,omitempty"` // OpenDeezer version
	Client   string   `json:"client,omitempty"`  // client/platform id (tui, macos, gnome…)
	Device   string   `json:"device,omitempty"`  // human device label ("OpenDeezer TUI")
	Commands []string `json:"commands"`          // supported command and event capabilities
}

Whoami is the unauthenticated identity and command-capability discovery response returned by GET /whoami. It carries the account display NAME (for the controller to recognise its own device) but never the user id: in same-account mode that id IS the credential, so echoing it here would let any bystander read and replay it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL