Packages
rockbox_ex
0.1.0
Idiomatic Elixir SDK for Rockbox — pipe-friendly, builder-friendly, with real-time event subscriptions and a plugin system.
Current section
Files
Jump to
Current section
Files
lib/rockbox.ex
defmodule Rockbox do
@moduledoc """
Idiomatic Elixir SDK for [Rockbox](https://www.rockbox.org).
## Quick start
client = Rockbox.new()
# Optional: open the WebSocket for real-time events
{:ok, _} = Rockbox.connect(client)
:ok = Rockbox.Events.subscribe(:track_changed)
# Look at what's playing
{:ok, track} = Rockbox.Playback.current_track(client)
IO.inspect(track)
# Search the library
{:ok, results} = Rockbox.Library.search(client, "radiohead")
# Play an album, shuffled
:ok = Rockbox.Playback.play_album(client, hd(results.albums).id, shuffle: true)
# React to track changes
receive do
{:rockbox, :track_changed, track} ->
IO.puts("▶ \#{track.title} — \#{track.artist}")
end
## Module map
| Domain | Module |
|-----------------------|----------------------------|
| Transport controls | `Rockbox.Playback` |
| Library / search | `Rockbox.Library` |
| Live queue | `Rockbox.Queue` |
| Saved playlists | `Rockbox.SavedPlaylists` |
| Smart playlists | `Rockbox.SmartPlaylists` |
| Smart-playlist rules | `Rockbox.SmartPlaylist.Rules` |
| Volume | `Rockbox.Sound` |
| Settings | `Rockbox.Settings` |
| System info | `Rockbox.System` |
| Filesystem browser | `Rockbox.Browse` |
| Output devices | `Rockbox.Devices` |
| Bluetooth (Linux) | `Rockbox.Bluetooth` |
| Real-time events | `Rockbox.Events` |
| Plugin behaviour | `Rockbox.Plugin` |
Every API function takes the client as its first argument so it composes
with the pipe operator. Functions that may fail return
`{:ok, value} | {:error, exception}` and have a matching `name!/N` variant
that raises on error.
"""
alias Rockbox.{Client, Plugins, Socket, Transport}
# ---------------------------------------------------------------------------
# Client construction
# ---------------------------------------------------------------------------
@doc """
Build a new client. See `Rockbox.Client` for the full option list.
iex> client = Rockbox.new()
iex> client.host
"localhost"
"""
@spec new(Client.opts()) :: Client.t()
def new(opts \\ []), do: Client.new(opts)
# ---------------------------------------------------------------------------
# Real-time subscriptions
# ---------------------------------------------------------------------------
@doc """
Open the WebSocket connection so subscribers start receiving events.
Idempotent — calling it twice for the same client returns the existing pid.
Subscribe with `Rockbox.Events.subscribe/1` (or `Rockbox.subscribe/1`).
"""
@spec connect(Client.t()) :: {:ok, pid()} | {:error, term()}
def connect(%Client{} = client) do
case Socket.whereis(client) do
nil ->
DynamicSupervisor.start_child(Rockbox.SocketSupervisor, {Socket, client})
pid ->
{:ok, pid}
end
end
@doc "Tear down the WebSocket connection for `client`."
@spec disconnect(Client.t()) :: :ok
def disconnect(%Client{} = client), do: Socket.stop(client)
@doc "Shortcut for `Rockbox.Events.subscribe/1`."
defdelegate subscribe(event), to: Rockbox.Events
@doc "Shortcut for `Rockbox.Events.unsubscribe/1`."
defdelegate unsubscribe(event), to: Rockbox.Events
# ---------------------------------------------------------------------------
# Plugins
# ---------------------------------------------------------------------------
@doc "Install a plugin module. See `Rockbox.Plugin` for the behaviour."
@spec use_plugin(Client.t(), module()) :: :ok | {:error, term()}
def use_plugin(%Client{} = client, plugin), do: Plugins.install(client, plugin)
@doc "Uninstall a plugin by module or by name string."
@spec unuse_plugin(module() | String.t()) :: :ok
def unuse_plugin(plugin), do: Plugins.uninstall(plugin)
@doc "List currently installed plugins."
@spec installed_plugins() :: [Plugins.entry()]
def installed_plugins, do: Plugins.list()
# ---------------------------------------------------------------------------
# Raw GraphQL escape hatch
# ---------------------------------------------------------------------------
@doc """
Run a raw GraphQL query or mutation. Variables can be a map or keyword list
(snake_case keys are converted to camelCase). Returns `{:ok, data}`.
Rockbox.query(client,
\"\"\"
query Album($id: String!) { album(id: $id) { id title artist } }
\"\"\",
id: "abc-123"
)
"""
@spec query(Client.t(), String.t(), map() | keyword() | nil) ::
{:ok, map()} | {:error, Exception.t()}
def query(%Client{} = client, gql, variables \\ nil),
do: Transport.execute(client, gql, variables)
@doc "Same as `query/3` but raises on error."
@spec query!(Client.t(), String.t(), map() | keyword() | nil) :: map()
def query!(%Client{} = client, gql, variables \\ nil),
do: Transport.execute!(client, gql, variables)
# ---------------------------------------------------------------------------
# Misc helpers
# ---------------------------------------------------------------------------
@doc """
Format a millisecond duration as `M:SS`.
iex> Rockbox.format_ms(75_000)
"1:15"
"""
@spec format_ms(integer()) :: String.t()
def format_ms(ms) when is_integer(ms) and ms >= 0 do
total = div(ms, 1000)
minutes = div(total, 60)
seconds = rem(total, 60)
"#{minutes}:#{seconds |> Integer.to_string() |> String.pad_leading(2, "0")}"
end
def format_ms(_), do: "0:00"
end