{"id":44027108,"url":"https://github.com/b0bbywan/go-odio-api","last_synced_at":"2026-04-02T12:13:50.048Z","repository":{"id":334618335,"uuid":"1142072809","full_name":"b0bbywan/go-odio-api","owner":"b0bbywan","description":"Unleash the power of Linux multimedia. Transform any Linux system into a smart, controllable  multimedia hub via simple REST API.","archived":false,"fork":false,"pushed_at":"2026-03-25T15:45:36.000Z","size":524,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-26T03:43:45.317Z","etag":null,"topics":["dbus","golang","mpris-dbus-interface","mpris2","pulseaudio","rest-api","systemd"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/b0bbywan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-25T22:48:03.000Z","updated_at":"2026-03-25T15:46:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/b0bbywan/go-odio-api","commit_stats":null,"previous_names":["b0bbywan/go-odio-api"],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/b0bbywan/go-odio-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b0bbywan%2Fgo-odio-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b0bbywan%2Fgo-odio-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b0bbywan%2Fgo-odio-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b0bbywan%2Fgo-odio-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/b0bbywan","download_url":"https://codeload.github.com/b0bbywan/go-odio-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b0bbywan%2Fgo-odio-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305981,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dbus","golang","mpris-dbus-interface","mpris2","pulseaudio","rest-api","systemd"],"created_at":"2026-02-07T18:10:58.902Z","updated_at":"2026-04-02T12:13:50.039Z","avatar_url":"https://github.com/b0bbywan.png","language":"Go","funding_links":["https://github.com/sponsors/b0bbywan"],"categories":[],"sub_categories":[],"readme":"# Odio API\n\n\u003e The universal remote for your Linux multimedia server\n\n[![CI](https://github.com/b0bbywan/go-odio-api/actions/workflows/ci.yml/badge.svg)](https://github.com/b0bbywan/go-odio-api/actions/workflows/ci.yml)\n[![Build](https://github.com/b0bbywan/go-odio-api/actions/workflows/build.yml/badge.svg)](https://github.com/b0bbywan/go-odio-api/actions/workflows/build.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/b0bbywan/go-odio-api)](https://goreportcard.com/report/github.com/b0bbywan/go-odio-api)\n[![GitHub Sponsors](https://img.shields.io/github/sponsors/b0bbywan?label=Sponsor\u0026logo=GitHub)](https://github.com/sponsors/b0bbywan)\n\n\u003e Part of the [odio](https://odio.love/) project.\n\nOdio is an ultra-lightweight Go daemon that exposes a single clean REST API over your Linux user session's D-Bus: MPRIS players (Spotify, VLC, Firefox, MPD, Kodi), PulseAudio/PipeWire, systemd user services, and power management. No root. No hacks. Just Linux primitives.\n\nBuilding a Linux multimedia setup is easy. Integrating it cleanly into Home Assistant always felt hacky, scattered integrations, SSH scripts, and fragile glue.\n\n\nTested on Fedora 43 Gnome, Debian 13 KDE, Raspbian 13, Openmediavault 8\nRaspberry Pi B through Pi 5.\nWorks without any system tweak.\n\n## Quick Start\n\n```bash\n# 1. Install\ncurl -fsSL https://apt.odio.love/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/odio.gpg\necho \"deb [signed-by=/usr/share/keyrings/odio.gpg] https://apt.odio.love stable main\" | sudo tee /etc/apt/sources.list.d/odio.list\nsudo apt update \u0026\u0026 sudo apt install odio-api\n\n# 2. Start\nsystemctl --user enable --now odio-api.service\n\n# 3. Test (start any MPRIS player first — Spotify, VLC, MPD…)\ncurl http://localhost:8018/players\ncurl http://localhost:8018/audio/server\n```\n\n→ See [Installation](#installation) for RPM, Docker, or building from source.\n\n## User Interface\n\n\u003cimg width=\"1658\" height=\"963\" alt=\"Capture d’écran du 2026-02-17 00-32-56\" src=\"https://github.com/user-attachments/assets/0a9697da-902b-41d0-8977-908ef66f1168\" /\u003e\n\nThe built-in Odio UI is accessible at:\n\n**http://localhost:8018/ui**\n(or http://your-host.local:8018/ui if zeroconf/mDNS is enabled)\n\nIt's a **100% local**, **responsive** (mobile + desktop), web interface designed to control your entire Linux multimedia setup from one place: MPRIS players, per-app/global volume, systemd user services, PipeWire/PulseAudio server, and more.\n\nThere's also an **[installable PWA](https://odio-pwa.vercel.app/)** to install on your phone/desktop to easily access your remote and navigate between several instances.\n\n[More info](UI.md)\n\n## Home Assistant Integration\n\n**[odio-ha](https://github.com/b0bbywan/odio-ha)** is the official Home Assistant integration for Odio.\n\nInstall via HACS → Custom Repositories → `https://github.com/b0bbywan/odio-ha`\n\nWhat it exposes as HA entities:\n- `media_player` — global PulseAudio/PipeWire audio receiver (volume, mute)\n- `media_player` per systemd service — power on/off, volume, state tracking (MPD, Kodi, shairport-sync, etc.)\n- MPRIS players — auto-discovered players with full playback control and metadata *(in progress)*\n\nOdio becomes the hub that makes all your HA integrations point to the correct machine. MPD service lifecycle managed by Odio, rich playback via HA's existing MPD integration — the two work together.\n\n## Use Cases\n\n| Setup | What Odio gives you |\n|---|---|\n| RPi music server (MPD + shairport-sync) | MPRIS control + restart services from HA |\n| HTPC / Kodi | Start/stop Kodi, MPRIS control via odio-ha |\n| Firefox kiosk (Netflix, YouTube) | Start/stop fake Netflix and Youtube app, MPRIS control via odio-ha |\n| Headless Spotify (spotifyd) | MPRIS playback + service lifecycle |\n| Any PulseAudio/PipeWire setup | Per-client and global volume/mute control |\n\n## Features\n\n### Media Player Control (MPRIS)\n\nAuto-discovers all MPRIS-compatible players in real time — Spotify, VLC, Firefox, MPD, Kodi, etc. Add a new player and it appears immediately, zero config.\n\n- Full playback control: play, pause, stop, next, previous\n- Volume, seek, and position control\n- Shuffle and loop mode management\n- Real-time state updates via D-Bus signals\n- Smart caching with automatic cache invalidation\n- Position heartbeat for accurate playback tracking\n\n### Audio Management (PulseAudio/PipeWire)\n\n- Server info and default output\n- Global and per-client volume/mute control\n- Real-time audio events via native PulseAudio monitoring\n- Limited PipeWire support via `pipewire-pulse`\n\n### Service Management (systemd)\n\nExplicit whitelist required — nothing managed unless listed in `config.yaml`.\n\n- List and monitor whitelisted systemd services (system + user)\n- Start, stop, restart, enable, disable user services\n- Real-time service state updates via D-Bus signals\n- Disabled by default\n\n⚠️ **Security model:** Odio enforces user-session mutations only at the application layer, regardless of D-Bus or polkit configuration. System units are strictly read-only. See [Security](#security) for full details.\n\n\n### Bluetooth Sink (A2DP)\n\nOdio can act as a Bluetooth audio receiver (A2DP sink) using D-Bus, allowing phones, computers, and other Bluetooth devices to stream audio to it.\n\n[Live example](UI.md#bluetooth-on-pi-b)\n\n[Inspired from my own Bluetooth setup since 2020](https://mathieu-requillart.medium.com/my-ultimate-guide-to-the-raspberry-pi-audio-server-i-wanted-bluetooth-64c347ee0d22)\n\n#### Configuration\nA few system configuration steps are required to make this work. Since Odio doesn't run as root, it can't do it by itself.\n\nFirst make sure the user running Odio belongs to `bluetooth` group\n\n```bash\n\n$ groups\npi adm dialout cdrom sudo audio video plugdev games users input render netdev bluetooth gpio i2c spi\n\n# if 'bluetooth' doesn't show in the line above:\n\n$ sudo usermod -a -G bluetooth \u003cusername\u003e\n```\n\nSome packages are needed to automatically plug PulseAudio or PipeWire to Bluetooth.\nOdio doesn't directly support `ALSA` and never will.\n\n```bash\n\n# PulseAudio\n$ sudo apt install pulseaudio-module-bluetooth\n\n# PipeWire\n$ sudo apt install libspa-0.2-bluetooth\n```\n\nTo ensure the device is correctly identified by phones and computers, you must edit `/etc/bluetooth/main.conf`:\n\n```ini\n[General]\nName=Odio       # Bluetooth name shown during device discovery\nClass=0x240428\n```\n\nClass of Device (CoD) breakdown:\n- `0x24` → Major Device Class: **Audio/Video**\n- `0x0428` → Minor + services :\n  - **Audio Sink**\n  - Loudspeaker\n  - Rendering device\n\nThis configuration makes Odio appear as a standard Bluetooth speaker or audio receiver.\n\nAfter modifying the configuration file, restart the Bluetooth service:\n```bash\n\n$ sudo systemctl restart bluetooth\n\n# A new user service should now be running\n# It creates an mpris player for each connected device\n$ systemctl --user status mpris-proxy.service\n● mpris-proxy.service - Bluetooth mpris proxy\n     Loaded: loaded (/usr/lib/systemd/user/mpris-proxy.service; enabled; preset: enabled)\n     Active: active (running) since Fri 2026-02-27 13:17:33 CET; 1h 15min ago\n Invocation: 4480169b9adb4c239ad81d7345dc1f92\n       Docs: man:mpris-proxy(1)\n   Main PID: 674 (mpris-proxy)\n      Tasks: 1 (limit: 379)\n        CPU: 791ms\n     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/mpris-proxy.service\n             └─674 /usr/bin/mpris-proxy\n\nfévr. 27 13:17:33 rasponkyold systemd[559]: Started mpris-proxy.service - Bluetooth mpris proxy.\n```\n\n#### Usage\n\nBluetooth is intentionally not left in an automatic or always-on state.\n\n- **Power up**:\n  Bluetooth is enabled, but the device is not discoverable. You can connect to it if your phone is already paired\n- **Power Down** Default 30min of inactivity (= no connected clients)\n- **Pairing mode**:\n  The device becomes visible to nearby Bluetooth devices and accepts new pairings.\n  After a successful pairing (or when the timeout expires), Bluetooth automatically returns to its normal state:\n    - Not discoverable\n    - Not pairable\n- Audio profile: **A2DP** (high-quality audio streaming).\n\nThis behavior matches how most Bluetooth speakers and audio receivers work.\n\nOdio automatically unblocks soft-blocked Bluetooth rfkill devices on power-up, so a `rfkill block bluetooth` followed by a power-up via the API will work without manual intervention.\n\nBonus: You get to control it through `/pulseaudio/clients` or `/players/` and in the UI !\n\n### Power Management\n\nRemote reboot and power-off via the REST API — no SSH needed for day-to-day operations. Disabled by default. Uses `org.freedesktop.login1` D-Bus interface.\n\n### Real-time Event Stream (SSE)\n\n`GET /events` streams live state changes to any HTTP client — no polling needed.\n\nEvents emitted:\n\n| Event type | Backend | Triggered by |\n|---|---|---|\n| `player.updated` | `mpris` | Playback state change, volume, metadata |\n| `player.added` | `mpris` | New MPRIS player appeared |\n| `player.removed` | `mpris` | MPRIS player closed |\n| `player.position` | `mpris` | Position tick (periodic, lightweight) |\n| `audio.updated` | `audio` | PulseAudio sink-input added or changed (volume, mute, cork) |\n| `audio.removed` | `audio` | PulseAudio sink-input removed |\n| `service.updated` | `systemd` | systemd unit state change |\n| `bluetooth.updated` | `bluetooth` | Bluetooth adapter or device state change (power, pairing, connection) |\n| `power.action` | `power` | Reboot or poweroff triggered via the API |\n\nSubscribe to a subset of events using query parameters:\n\n| Parameter | Description | Example |\n|---|---|---|\n| `types` | Comma-separated event type names to include | `?types=player.updated,player.added` |\n| `backend` | Comma-separated backend names to include | `?backend=mpris,audio` |\n| `exclude` | Comma-separated event type names to exclude | `?exclude=player.position` |\n| `keepalive` | Keepalive interval in seconds (default `30`, min `10`, max `120`) | `?keepalive=60` |\n\n`types` and `backend` can be combined — the union of all matched types is used. Omitting both receives all events. `server.info` is always delivered and cannot be excluded.\n\n### REST API\n\n- `\u003c50ms` p95 response time, `0%` CPU on idle — tested on Raspberry Pi B and B+\n- Localhost binding by default, configurable per network interface\n- Zeroconf/mDNS auto-discovery on the LAN (opt-in)\n\n## Platform Support\n\n| Architecture | Package | Tested on |\n|---|---|---|\n| amd64 | deb, rpm | Fedora 43 Gnome, Debian 13 KDE |\n| arm64 | deb, rpm | Raspberry Pi 3/4/5 (64-bit) |\n| armv7hf | deb, rpm | Raspberry Pi 2/3 (32-bit) |\n| **armhf (ARMv6)** | deb, rpm | **Raspberry Pi B / B+ / Zero** |\n\nPre-built packages (amd64, arm64, armv7hf, armhf/ARMv6) and a multi-arch Docker image (amd64, arm64, arm/v7) are available on every build. Docker does not target arm/v6 — Pi B/Zero users should use the armhf package.\n\n## Roadmap\n\n- Wayland Remote Control, Authentication, Photos Casting...\n\n## Installation\n\n### APT Repository (Debian / Raspberry Pi OS)\n\n```bash\ncurl -fsSL https://apt.odio.love/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/odio.gpg\necho \"deb [signed-by=/usr/share/keyrings/odio.gpg] https://apt.odio.love stable main\" | sudo tee /etc/apt/sources.list.d/odio.list\nsudo apt update\nsudo apt install odio-api\n```\n\n### Packages (deb / rpm)\n\nPre-built packages for amd64, arm64, armv7hf, and armhf (ARMv6) are available as artifacts on each [build workflow run](https://github.com/b0bbywan/go-odio-api/actions/workflows/build.yml).\n\n```bash\n# Debian/Ubuntu/Raspberry Pi OS\nsudo dpkg -i odio-api_\u003cversion\u003e_amd64.deb\n\n# Fedora/RHEL\nsudo rpm -i odio-api-\u003cversion\u003e.x86_64.rpm\n```\n\n### From Source\n\n```bash\ngit clone https://github.com/b0bbywan/go-odio-api.git\ncd go-odio-api\ntask build    # builds CSS + Go binary with version from git\n./bin/odio-api\n```\n\n### systemd User Service\n\nCreate `~/.config/systemd/user/odio-api.service`:\n\n```ini\n[Unit]\nDescription=Dbus api for Odio\nDocumentation=https://github.com/b0bbywan/go-odio-api\nWants=sound.target\nAfter=sound.target\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nExecStart=/usr/bin/odio-api\nRestart=always\nRestartSec=12\nTimeoutSec=30\n\n[Install]\nWantedBy=default.target\n```\n\n```bash\nsystemctl --user daemon-reload\nsystemctl --user enable odio-api.service\nsystemctl --user start odio-api.service\n```\n\n**Headless systems:** Enable lingering so the user session (PulseAudio/PipeWire, D-Bus, `XDG_RUNTIME_DIR`) survives without an active login:\n\n```bash\nsudo loginctl enable-linger \u003cusername\u003e\n```\n\n### Docker\n\nA pre-built multi-arch image is available on GHCR (amd64, arm64, arm/v7):\n\n```\nghcr.io/b0bbywan/go-odio-api:latest\n```\n\n#### Quick start\n\n```bash\n# 1. Prepare configuration (bind: all required for Docker)\ncp share/config.yaml config.yaml\n# Edit config.yaml: set bind: all\n\n# 3. (Optional) Only needed if docker compose config shows wrong paths\ncp .env.example .env\n\n# 4. Start\ndocker compose up -d\n```\n\nThe `docker-compose.yml` reads `UID`, `XDG_RUNTIME_DIR`, `HOME` and `DBUS_SESSION_BUS_ADDRESS`\ndirectly from your shell environment — no configuration needed for a standard Linux setup.\nSee `.env.example` if your shell doesn't export these automatically (e.g. fish).\n\nEnvironment variables passed to the container:\n\n| Variable | Source | Purpose |\n|---|---|---|\n| `XDG_RUNTIME_DIR` | host env → fallback `/run/user/$UID` | D-Bus and PulseAudio runtime directory |\n| `DBUS_SESSION_BUS_ADDRESS` | host env → fallback derived from `XDG_RUNTIME_DIR` | User D-Bus session socket |\n| `HOME` | host env → fallback `/home/odio` | PulseAudio cookie lookup path |\n\nVolumes mounted (all read-only):\n\n| Volume | Purpose |\n|---|---|\n| `./config.yaml` | odio configuration |\n| `$XDG_RUNTIME_DIR/bus` | user D-Bus session socket |\n| `$XDG_RUNTIME_DIR/systemd` | user systemd folder (utmp unavailable) |\n| `/run/utmp` | user systemd monitoring (utmp available) |\n| `/var/run/dbus/system_bus_socket` | system D-Bus socket |\n| `$XDG_RUNTIME_DIR/pulse` | PulseAudio socket |\n| `$HOME/.config/pulse/cookie` | PulseAudio cookie |\n\n**Note:** `bind` must be set to `all` in `config.yaml` for Docker remote access (bridge network). Zeroconf won't work in bridge network mode. Host network mode is strongly discouraged.\n\nTo build locally instead:\n```bash\ndocker build -t odio-api .\n# or simply: task docker:build\n```\nThe Docker build is fully self-contained — Tailwind CSS is downloaded and compiled inside the builder stage.\n\n#### Command-line Flags\n\n- `--config \u003cpath\u003e` — specify a custom YAML configuration file\n- `--version` — print version and exit\n- `--help` — show help message\n\n## Configuration\n\nConfiguration file locations (in order of precedence):\n- Specified with `--config \u003cpath\u003e`\n- `~/.config/odio-api/config.yaml` (user-specific)\n- `/etc/odio-api/config.yaml` (system-wide)\n- A default configuration is available in `share/config.yaml`\n\nDisabling a backend disables the backend and all its routes.\n\n```yaml\nbind: lo\nlogLevel: info\n\napi:\n  enabled: true\n  port: 8018\n  ui:\n    enabled: true\n  cors:\n    origins: [\"https://odio-pwa.vercel.app\"] # default for PWA\n    # origins: [\"https://app.example.com\"]  # specific origins\n```\n\n### Backend configuration examples\n\n#### Network binding\n\n```yaml\nbind: lo                      # loopback only (default)\n# bind: enp2s0                # single LAN interface\n# bind: [lo, enp2s0]          # loopback + LAN (required for UI access from the network)\n# bind: [lo, enp2s0, wlan0]   # loopback + ethernet + wifi\n# bind: all                   # all interfaces — 0.0.0.0 (Docker, remote access)\n```\n\n**Note:** The built-in web UI requires `lo` to be in the bind list. If `lo` is absent, the UI is automatically disabled.\n\n#### systemd (opt-in, whitelist required)\n\n```yaml\nsystemd:\n  enabled: true\n  system:\n    - bluetooth.service\n    - upmpdcli.service\n  user:\n    - pipewire-pulse.service\n    - pulseaudio.service\n    - mpd.service                       # see [1]\n    - shairport-sync.service            # see [2]\n    - snapclient.service                # incompatible with mpris\n    - spotifyd.service                  # see [3]\n    - firefox-kiosk@netflix.com.service # default support for mpris\n    - firefox-kiosk@youtube.com.service # default support for mpris\n    - firefox-kiosk@my.home-assistant.io.service\n    - kodi.service                      # see [4]\n    - vlc.service                       # default support for mpris\n    - plex.service                      # see [5]\n```\n[1] Install `mpd-mpris` or `mpDris2` for MPRIS support\n[2] Check my [article on Medium: Shairport Sync/Airplay with PulseAudio and MPRIS support](https://medium.com/@mathieu-requillart/set-up-a-b83d9c980e75)\n[3] Default on desktop; on headless, your spotifyd version [must be built with MPRIS support](https://docs.spotifyd.rs/advanced/dbus.html)\n[4] Install [Kodi Add-on: MPRIS D-Bus interface](https://github.com/wastis/MediaPlayerRemoteInterface#)\n[5] Maybe supported, untested\n\n#### Bluetooth\n\n```yaml\nbluetooth:\n  enabled: true\n  timeout: 5s\n  pairingTimeout: 60s\n  idleTimeout: 30m # 0 for no autopoweroff\n```\n\n#### Power Management\n\n```yaml\npower:\n  enabled: true\n  capabilities:\n    poweroff: true\n    reboot: true\n```\n\n#### Zeroconf / mDNS\n\n```yaml\nbind: eno1\nzeroconf:\n  enabled: true\n```\n\nOdio advertises itself via mDNS. Look for `_http._tcp.local.` → instance `odio-api`. Disabled on `lo` binding.\n\n### Security defaults\n\n- **Localhost binding by default** — prevents accidental network exposure\n- **Systemd disabled by default** — service control must be explicitly enabled and configured\n- **Read-only Docker mounts** — all volume mounts are read-only in the provided `docker-compose.yml`\n- **Zeroconf opt-in** — must be enabled, then mDNS adapts to `bind`: disabled on `lo`, enabled on specific interfaces, or `all` interfaces without `lo`\n\n## API Endpoints\n\n### Server Information\n\n```\nGET    /server                             # {\"hostname\":\"\",\"os_platform\":\"\",\"os_version\":\"\",\"api_sw\":\"\",\"api_version\":\"\",\"backends\":{\"mpris\":true,\"pulseaudio\":true,\"systemd\":false,\"zeroconf\":false}}\n```\n\n### MPRIS Media Players\n\n```\nGET    /players                           # List all media players\nPOST   /players/{player}/play             # Play\nPOST   /players/{player}/pause            # Pause\nPOST   /players/{player}/play_pause       # Toggle play/pause\nPOST   /players/{player}/stop             # Stop\nPOST   /players/{player}/next             # Next track\nPOST   /players/{player}/previous         # Previous track\nPOST   /players/{player}/seek             # Seek (body: {\"offset\": 1000000})\nPOST   /players/{player}/position         # Set position (body: {\"track_id\": \"...\", \"position\": 0})\nPOST   /players/{player}/volume           # Set volume (body: {\"volume\": 0.5})\nPOST   /players/{player}/loop             # Set loop status (body: {\"loop\": \"None|Track|Playlist\"})\nPOST   /players/{player}/shuffle          # Set shuffle (body: {\"shuffle\": true})\n```\n\n### PulseAudio\n\n```\nGET    /audio                             # Combined: server info, outputs, clients\nGET    /audio/server                      # Get server info\nPOST   /audio/server/mute                 # Mute/unmute default output\nPOST   /audio/server/volume               # Set default output volume (body: {\"volume\": 0.5})\nGET    /audio/clients                     # List audio clients (sink-inputs)\nPOST   /audio/clients/{sink}/mute         # Mute/unmute client\nPOST   /audio/clients/{sink}/volume       # Set client volume (body: {\"volume\": 0.5})\nGET    /audio/outputs                     # List all audio outputs (sinks)\nPOST   /audio/outputs/{output}/default    # Set default output\nPOST   /audio/outputs/{output}/mute       # Mute/unmute output\nPOST   /audio/outputs/{output}/volume     # Set output volume (body: {\"volume\": 0.5})\nGET    /audio/cookie                      # Download PulseAudio cookie file (requires pulseaudio.serve_cookie: true)\n```\n\n### Systemd Services\n\n```\nGET    /services                          # List all monitored services\nPOST   /services/{scope}/{unit}/start     # Start service (scope: system|user)\nPOST   /services/{scope}/{unit}/stop      # Stop service (scope: system|user)\nPOST   /services/{scope}/{unit}/restart   # Restart service\nPOST   /services/{scope}/{unit}/enable    # Enable service (scope: system|user)\nPOST   /services/{scope}/{unit}/disable   # Disable service\n```\n\n### Bluetooth Sink\n```\nGET    /bluetooth                         # Get Bluetooth status (powered, pairing mode state)\nPOST   /bluetooth/power_up                # Turns Bluetooth on and makes the device ready to connect to already paired devices.\nPOST   /bluetooth/power_down              # Turns Bluetooth off and disconnects any active Bluetooth connections.\nPOST   /bluetooth/pairing_mode            # Enables Bluetooth pairing mode for 60s (configurable).\n                                          # Returns to non-discoverable state after timeout or successful pairing.\n\n```\n\n### Power Management\n\n```\nGET    /power/                            # Power capabilities {\"reboot\": true, \"power_off\": false}\nPOST   /power/power_off                   # Poweroff (403 if not declared in capabilities)\nPOST   /power/reboot                      # Reboot (403 if not declared in capabilities)\n```\n\n### SSE Event Stream\n\n```\nGET    /events                                        # All events (text/event-stream)\nGET    /events?backend=mpris                          # Only MPRIS player events\nGET    /events?backend=mpris,audio                    # Player + audio events\nGET    /events?backend=bluetooth                      # Only Bluetooth state changes\nGET    /events?backend=power                          # Only power actions (reboot/poweroff)\nGET    /events?types=player.updated                   # Specific event types\nGET    /events?exclude=player.position                # All events except position ticks\nGET    /events?keepalive=60                           # Custom keepalive interval (seconds)\n\nGET    /events?types=player.updated,service.updated\u0026backend=audio\u0026exclude=player.position  # Mixed\n```\n\n#### Testing with curl\n\n```bash\n# All events\ncurl -N http://localhost:8018/events\n\n# Only player events\ncurl -N \"http://localhost:8018/events?backend=mpris\"\n\n# Only position ticks lightweight on purpose (e.g. to drive a seek bar)\ncurl -N \"http://localhost:8018/events?types=player.position\"\n```\n\nExpected output:\n\n```\nevent: server.info\ndata: \"connected\"\n\nevent: player.updated\ndata: {\"bus_name\":\"org.mpris.MediaPlayer2.spotify\",\"identity\":\"Spotify\",...}\n\nevent: audio.updated\ndata: [{\"id\":42,\"name\":\"Spotify\",\"volume\":0.75,\"muted\":false,...}]\n\nevent: audio.removed\ndata: [{\"id\":41,\"name\":\"pactl\",\"volume\":1,...}]\n\nevent: service.updated\ndata: {\"name\":\"mpd.service\",\"scope\":\"user\",\"active_state\":\"active\",\"running\":true,...}\n\nevent: bluetooth.updated\ndata: {\"powered\":true,\"discoverable\":false,\"pairable\":false,\"pairing_active\":false,\"known_devices\":[{\"address\":\"AA:BB:CC:DD:EE:FF\",\"name\":\"My Phone\",\"trusted\":true,\"connected\":true}]}\n\nevent: power.action\ndata: {\"action\":\"reboot\"}\n```\n\n#### Simple browser listener\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\u003ctitle\u003eOdio live events\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n\u003cpre id=\"log\"\u003e\u003c/pre\u003e\n\u003cscript\u003e\n  const log = document.getElementById('log');\n\n  // Subscribe to all events — add ?backend=mpris or ?types=... to filter\n  const es  = new EventSource('http://localhost:8018/events');\n\n  ['player.updated', 'player.added', 'player.removed', 'player.position',\n   'audio.updated', 'audio.removed', 'service.updated', 'bluetooth.updated', 'power.action'].forEach(type =\u003e {\n    es.addEventListener(type, e =\u003e {\n      const entry = `[${type}] ${e.data}\\n`;\n      log.textContent = entry + log.textContent;\n    });\n  });\n\n  es.onerror = () =\u003e log.textContent = '[error] connection lost\\n' + log.textContent;\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nSave as `events.html`, open in a browser — events appear live as they happen. No polling, no page refresh needed.\n\n## Security\n\n### systemd backend\n\n⚠️ **Security Notice**\n\nSystemd control is disabled by default and requires an explicit whitelist. Odio mitigates risks with deliberate security design:\n\n- **Disabled by default** — must explicitly set `systemd.enabled: true` AND configure units. Empty config → auto-disabled even with `enabled: true`.\n- **Localhost only** — API binds to `lo` by default. Never expose to untrusted networks or the Internet.\n- **User-only mutations** — start/stop/restart/enable/disable only work on user D-Bus. System units are strictly read-only, enforced at the application layer regardless of D-Bus or polkit configuration. This protects against misconfigured or compromised D-Bus setups.\n- **Root forbidden by design** — Odio refuses to run as root.\n- **No preconfigured units** — nothing managed unless explicitly listed.\n\n- **MPRIS Backend**: Communicates with media players via D-Bus, implements smart caching and real-time updates through D-Bus signals\n- **PulseAudio Backend**: Interacts with PulseAudio/PipeWire for audio control, supports real-time event monitoring\n- **Systemd Backend**: Manages systemd services via D-Bus with native signal-based monitoring\n**You must knowingly enable this at your own risk.** Odio is free software and comes with no warranty.\n- **Bluetooth Backend**: Act as a Bluetooth audio receiver (A2DP sink) via D-Bus\n\n### REST API\n\n⚠️ **Security Notice:** No authentication mechanism is provided. **Never expose this API to untrusted networks or the Internet.** Designed for localhost or trusted LAN use only.\n\n## Architecture\n\n### Key Design: The User Session\n\nAll multimedia services run as systemd user units, not system-wide daemons. This unlocks a single, unified D-Bus session bus where PulseAudio/PipeWire, MPRIS players, and user systemd units all coexist. Odio listens to that bus and exposes everything via HTTP. Add a new MPRIS player — it appears immediately, zero code or config change.\n\n### Backends\n\n- **MPRIS Backend** — D-Bus communication with media players, smart caching, real-time D-Bus signal updates\n- **PulseAudio Backend** — native PulseAudio protocol (pure Go, no libpulse), real-time event monitoring\n- **Systemd Backend** — D-Bus with filesystem monitoring fallback (`/run/user/{uid}/systemd/units`)\n- **Power Backend** — `org.freedesktop.login1` D-Bus interface\n\n### Performance\n\n- Caching reduces D-Bus calls by ~90%\n- D-Bus signal-based updates instead of polling\n- Batch property retrieval\n- Automatic heartbeat management for position tracking\n- Connection pooling and timeout handling\n\n## Development\n\n### Prerequisites\n\n- Go 1.24 or higher\n\n### Running Tests\n\n```bash\ngo test ./...\ngo test -cover ./...\n\ngo test ./backend/mpris/...\ngo test ./backend/pulseaudio/...\ngo test ./backend/systemd/...\n```\n\n### Building\n\nThe project uses [Task](https://taskfile.dev) for build automation.\n\n```bash\n# Install Task (once)\ngo install github.com/go-task/task/v3/cmd/task@latest\n\n# Build for the current host (CSS + Go binary, version from git)\ntask build\n\n# Cross-compile for all supported architectures (output: dist/)\ntask build:all-arch\n\n# Individual targets\ntask build:linux-amd64     # x86_64\ntask build:linux-arm64     # RPi 3/4/5 64-bit\ntask build:linux-armv7hf   # RPi 2/3 32-bit (ARMv7)\ntask build:linux-armhf     # RPi B/B+/Zero (ARMv6, RPi OS armhf)\n\n# CSS only\ntask css              # Ensure CSS is available (compile or download from CDN)\ntask css-local        # Compile locally (requires Tailwind CLI)\ntask css:watch        # Watch mode for development\n```\n\n**Note:** `task build` injects the version via `-ldflags` from `git describe`. The version is visible via `./bin/odio-api --version`.\n\n#### CSS Build Strategy\n\nThe UI uses Tailwind CSS with an intelligent multi-architecture build strategy:\n\n- **Development (x64/arm64/armv7)** — `task build` compiles CSS locally using Tailwind CLI\n- **Legacy ARM (ARMv6 — Raspberry Pi B/B+)** — `task build` downloads pre-built CSS from CDN (`https://bobbywan.me/odio-css/`)\n\nTailwind CLI doesn't provide ARMv6 binaries. The CSS is architecture-independent, so it's compiled on x64 and distributed via CDN.\n\n**CDN structure:**\n```\nhttps://bobbywan.me/odio-css/\n  main/abc1234.css          # commit-specific\n  main/latest.css           # latest for branch\n  tags/v0.6.0.css           # release tags (never cleaned)\n```\n\nCSS files are **not** committed to the repository.\n\n### Packaging (deb / rpm)\n\nPackages are built with [nfpm](https://nfpm.goreleaser.com/) via Task.\n\n```bash\n# Install nfpm (once)\ngo install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest\n\n# Build all packages for all architectures (output: dist/)\ntask package:all\n\n# Individual targets\ntask package:deb:linux-amd64     # .deb amd64\ntask package:deb:linux-arm64     # .deb arm64\ntask package:deb:linux-armv7hf   # .deb armv7hf\ntask package:deb:linux-armhf     # .deb armhf (ARMv6, RPi OS)\ntask package:rpm:linux-amd64     # .rpm x86_64\ntask package:rpm:linux-arm64     # .rpm aarch64\ntask package:rpm:linux-armv7hf   # .rpm armv7hl\ntask package:rpm:linux-armhf     # .rpm armv6hl\n```\n\n## Dependencies\n\n- [spf13/viper](https://github.com/spf13/viper) — configuration\n- [godbus/dbus](https://github.com/godbus/dbus) — D-Bus bindings\n- [coreos/go-systemd](https://github.com/coreos/go-systemd) — systemd D-Bus bindings\n- [the-jonsey/pulseaudio](https://github.com/the-jonsey/pulseaudio) — pure-Go PulseAudio native protocol (no libpulse)\n- [grandcat/zeroconf](https://github.com/grandcat/zeroconf) — mDNS / DNS-SD\n- [HTMX](https://htmx.org/)\n- [TailwindCSS](https://tailwindcss.com/)\n\n## Contributing\n\nOdio was first pushed on January 25, 2026. It's early stage. v0.4 works out of the box, but there's a long road ahead. Expect bugs.\n\n**Does it work on your setup? What breaks? What's missing?**\n\nTry it. Tell me what works and what doesn't. Show me your setup. If you want to contribute code, even better. Go is a great language for this use case.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add some amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\nFor issues and questions: [GitHub repository](https://github.com/b0bbywan/go-odio-api)\n\n## License\n\nBSD 2-Clause License — see the LICENSE file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fb0bbywan%2Fgo-odio-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fb0bbywan%2Fgo-odio-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fb0bbywan%2Fgo-odio-api/lists"}