Skip to content

Repository files navigation

Poolsuite FM for Roon / Bluesound / Sonos

Stream the impeccable vibes of Poolsuite FM — the retro-styled, SoundCloud-powered internet radio — directly to your Roon, Bluesound, or Sonos home audio system.

This bridge runs a local internet radio station that Roon picks up natively (and Bluesound / Sonos can access via TuneIn) as a Live Radio stream, complete with track metadata so your player of choice can display artist info, album art, and link to matching content in Tidal or Qobuz.

This package also runs a web browser GUI to control your local radio station, which also can change what is streamed to Roon. (screenshots below)
Poolsuite FM
Dark Mode Light Mode
Dark Mode Light Mode

Web UI Demo

How It Works

Poolsuite API → yt-dlp (resolve SoundCloud) → ffmpeg (transcode) → HTTP MP3 stream → Roon
  1. Fetches curated playlists from the Poolsuite API
  2. Resolves SoundCloud tracks to direct audio URLs using yt-dlp
  3. Transcodes to a constant-bitrate MP3 stream via ffmpeg
  4. Serves the stream at http://YOUR_IP:8489/stream
  5. Roon connects and plays it on any zone — with full DSP, grouping, and volume control

Track metadata is injected via the ICY protocol, so Roon displays the current artist and song title in real time and can cross-reference against Tidal/Qobuz for rich metadata.

Requirements

Dependency Version Purpose
Python 3.9+ Runtime
ffmpeg Any recent Audio transcoding (must include libmp3lame)
yt-dlp Latest recommended Resolves SoundCloud URLs to direct streams

macOS

brew install python ffmpeg yt-dlp

Ubuntu / Debian

sudo apt update && sudo apt install -y python3 python3-pip ffmpeg
pip3 install yt-dlp

Arch Linux

sudo pacman -S python python-pip ffmpeg yt-dlp

Windows (WSL recommended)

Install WSL, then follow the Ubuntu instructions above.

Quick Start

# Clone the repo
git clone https://github.com/thepeterberg/Poolsuite_Roon_Streaming.git
cd poolsuite-roon-streaming

# Install Python dependencies
pip3 install -r requirements.txt

# Start the bridge
python3 main.py

You'll see:

============================================================
  Poolsuite -> Roon Bridge
============================================================

  Stream URL:  http://YOUR_LOCAL_IP:8489/stream
  Status:      http://YOUR_LOCAL_IP:8489/status
  Web UI:      http://YOUR_LOCAL_IP:8489/

  Add the stream URL as a Live Radio station in Roon:
    Roon > My Live Radio > + > paste the stream URL

============================================================

Adding to Roon

  1. Find your machine's local IP:
    # macOS
    ipconfig getifaddr en0
    
    # Linux
    hostname -I | awk '{print $1}'
  2. Open Roon on any client
  3. Go to My Live Radio in the sidebar
  4. Click + Add Station
  5. Paste: http://YOUR_IP:8489/stream
  6. Name it Poolsuite FM
  7. Play on any zone

Roon treats this like any internet radio station — zone grouping, volume, DSP, and signal path all work normally.

Adding to Bluesound

Bluesound players can play any MP3 stream URL directly via the BluOS app — no TuneIn account needed.

  1. Find your machine's local IP (see above)
  2. Open the BluOS app on your phone, tablet, or desktop
  3. Tap the Browse tab (bottom nav)
  4. Tap TuneIn (or Radio Paradise — either works, we just need to get to the custom URL option)
  5. Instead of browsing TuneIn, tap the three dots menu (top right) and select Add Custom Station
  6. Enter the stream URL: http://YOUR_IP:8489/stream
  7. Name it Poolsuite FM
  8. Tap Save

The station now appears in your My Presets or My Radio Stations list and can be played on any Bluesound player or group.

Alternative — direct play via URL: If you don't see the custom station option, you can use the BluOS API directly. Open a browser and navigate to:

http://BLUESOUND_IP:11000/Play?url=http://YOUR_IP:8489/stream&preset_id=1&image=/images/radio.png

Replace BLUESOUND_IP with your Bluesound player's IP address. This starts playback immediately.

Note: ICY metadata (track titles) works with Bluesound — you'll see the current artist and song on the player display and in the BluOS app.

Adding to Sonos

Sonos can play any MP3 stream via the TuneIn "My Radio Stations" feature. This uses TuneIn's website to register your local stream URL, which then syncs to your Sonos system.

Option 1: Via TuneIn (recommended)

  1. Find your machine's local IP (see above)
  2. Go to tunein.com and sign in (create a free account if needed)
  3. Make sure your Sonos system is linked to the same TuneIn account (Sonos app > Settings > Services & Voice > TuneIn)
  4. On the TuneIn website, go to My Profile > My Custom URLs (or visit https://tunein.com/user/custom-url/)
  5. Click Add Custom URL
  6. Enter the stream URL: http://YOUR_IP:8489/stream
  7. Name it Poolsuite FM
  8. Save

Now in the Sonos app:

  1. Go to Browse > TuneIn > My Radio Stations
  2. Poolsuite FM should appear in the list
  3. Tap to play on any room or group

Important: Your Sonos speakers need to be able to reach the machine running the bridge on your local network. The TuneIn custom URL is just a pointer — the audio streams directly from your machine to the Sonos speakers.

Option 2: Direct play via Sonos API

If you have the Sonos HTTP API (e.g. node-sonos-http-api) running, you can start playback directly:

curl "http://SONOS_API_IP:5005/ROOM_NAME/setavtransporturi/http://YOUR_IP:8489/stream"

Option 3: Via the Sonos desktop controller

  1. Open the Sonos desktop app (Windows or macOS — not the mobile app)
  2. Go to Manage > Add Radio Station
  3. Enter the stream URL: http://YOUR_IP:8489/stream
  4. Name it Poolsuite FM
  5. Click OK

The station appears under My Radio Stations and is accessible from all Sonos apps.

Note: Sonos supports ICY metadata, so track titles will display in the app and on Sonos displays (e.g. Era 300, Move 2).

CLI Options

python3 main.py [options]

Options:
  -c, --config FILE     Path to config JSON file
  -p, --port PORT       HTTP server port (default: 8489)
  --host HOST           Bind address (default: 0.0.0.0)
  --no-shuffle          Play tracks in playlist order
  --playlist NAME       Filter to a specific Poolsuite playlist
  -v, --verbose         Enable debug logging

Examples

# Verbose logging (recommended for first run)
python3 main.py -v

# Custom port
python3 main.py --port 9000

# Only play tracks from the "Indie" channel
python3 main.py --playlist "Indie"

# Use a config file
python3 main.py --config config.json

Web UI & Controls

The bridge includes a retro Miami 80s-styled stereo interface at http://YOUR_IP:8489/ with dark and light themes:

  • LCD display with scrolling marquee showing the current track (linked to SoundCloud)
  • Transport controls — Previous, Play/Pause, Next
  • Analog channel selector dial that animates when switching channels
  • Channel buttons to switch between Poolsuite playlists on the fly
  • In-browser audio player for listening directly
  • Play history with SoundCloud links for each track
  • Copy-to-clipboard buttons for all endpoint URLs
Endpoint Description
/ Web UI — retro stereo interface with transport controls and play history
/stream MP3 audio stream (this is what you add to Roon)
/stream.mp3 Alias for /stream
/skip Skip to the next track (GET or POST)
/prev Go back to the previous track (GET or POST)
/channel?name=X Switch channel; omit name to list available channels (GET)
/status JSON API — now playing, listeners, uptime

Track Controls

Roon's transport controls (next/previous) don't work with radio streams. Use these instead:

  • Web UI: Open http://YOUR_IP:8489/ and use the PREV / NEXT transport buttons
  • API: curl http://YOUR_IP:8489/skip or curl http://YOUR_IP:8489/prev
  • macOS Shortcut: Create a Shortcuts automation that fetches the /skip or /prev URL, then assign a keyboard shortcut
  • Home Assistant / Streamdeck: Call the /skip or /prev endpoint as an HTTP action

Channel Switching

Switch between Poolsuite playlists without restarting:

  • Web UI: Click any channel button below the dial, or use the --playlist CLI flag for the initial channel
  • API: curl http://YOUR_IP:8489/channel?name=Indie
  • List channels: curl http://YOUR_IP:8489/channel (returns JSON with available channels and current selection)

Configuration

Copy config.example.json to config.json and edit as needed:

{
  "host": "0.0.0.0",
  "port": 8489,
  "bitrate": "192k",
  "crossfade_seconds": 3,
  "shuffle": true,
  "playlist_filter": null
}
Key Default Description
host "0.0.0.0" Bind address
port 8489 HTTP server port
bitrate "192k" MP3 output bitrate
format "mp3" Output audio format
crossfade_seconds 2 Seconds of silence between tracks
shuffle true Randomize track order
poolsuite_api "https://api.poolsidefm.workers.dev" Poolsuite API base URL
playlist_filter null Only play tracks from playlists matching this name

Running as a Background Service

systemd (Linux)

sudo tee /etc/systemd/system/poolsuite-roon.service << 'EOF'
[Unit]
Description=Poolsuite FM for Roon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/path/to/poolsuite-roon-streaming
ExecStart=/usr/bin/python3 main.py --config config.json
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now poolsuite-roon.service

# Check status
sudo systemctl status poolsuite-roon.service

# View logs
journalctl -u poolsuite-roon.service -f

launchd (macOS)

cat > ~/Library/LaunchAgents/com.poolsuite.roon.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.poolsuite.roon</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/python3</string>
        <string>main.py</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/path/to/poolsuite-roon-streaming</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>
EOF

launchctl load ~/Library/LaunchAgents/com.poolsuite.roon.plist

Architecture

┌──────────────────────────────────────────────────────┐
│  main.py — Orchestrator                              │
│  Fetches playlists, resolves tracks, manages queue    │
│  Pre-resolves next track while current plays          │
│  Feeds realtime silence to keep stream alive in gaps  │
│  Handles skip, previous track, and channel switching  │
├──────────────────────────────────────────────────────┤
│  poolsuite_client.py — Poolsuite API Client           │
│  GET /v1/get_tracks_by_playlist → playlist + tracks   │
│  GET /v2/get_sc_mp3_stream?track_id=X → audio URL     │
│  Rate limiting with exponential backoff               │
├──────────────────────────────────────────────────────┤
│  audio_pipeline.py — Audio Pipeline                   │
│  yt-dlp: resolve SoundCloud → direct audio URL        │
│  ffmpeg master encoder: continuous 192k MP3 stream    │
│  ffmpeg decoder: per-track PCM fed into master encoder│
├──────────────────────────────────────────────────────┤
│  stream_server.py — HTTP Radio Server                 │
│  /stream: continuous MP3 with ICY metadata injection   │
│  /skip, /prev: track navigation                       │
│  /channel: playlist switching                         │
│  /status: JSON now-playing info                       │
│  /: retro stereo web UI with transport + history      │
├──────────────────────────────────────────────────────┤
│  template.html — Retro Miami 80s Web UI               │
│  LCD marquee, transport controls, analog channel dial │
│  Play history with SoundCloud links, dark/light theme │
└──────────────────────────────────────────────────────┘
          │
          ▼
    Roon (Live Radio)

Troubleshooting

"yt-dlp not found" / "ffmpeg not found" Install the missing dependency. Keep yt-dlp updated — SoundCloud extractors break periodically:

pip3 install -U yt-dlp

Tracks skipping or failing to resolve SoundCloud URLs expire and rate limits apply. The bridge retries with backoff and pre-resolves the next track to minimize gaps. Run with -v for detailed logs.

"Address already in use" on startup A previous instance is still running. Kill it:

lsof -ti :8489 | xargs kill -9

Roon says "could not find a radio station at this URL"

  • Make sure the bridge is running and tracks are actively streaming (check the terminal logs)
  • Use your machine's actual local IP, not 0.0.0.0 or localhost
  • Ensure your Roon Core can reach the bridge (same network, port not firewalled)
  • Test by opening http://YOUR_IP:8489/ in a browser first

Stream dies between tracks This was a known issue that's been fixed. Make sure you're on the latest version. The bridge now pre-resolves the next track and pumps silence during transitions.

Rate limiting (429 errors in logs) The bridge automatically retries with exponential backoff (2s, 4s, 8s). If you see persistent 429s, SoundCloud is throttling aggressively — the bridge will recover on its own.

Poolsuite Channels

The Poolsuite API provides several curated channels. Use --playlist to filter:

  • Poolsuite FM — The flagship mix
  • Indie — Indie poolside vibes
  • Balearic — Mediterranean chill
  • Tokyo — Japanese city pop and funk
  • Friday — Weekend starters
  • Hangover — Sunday recovery
  • Mixtapes — Guest-curated long mixes

Credits

License

MIT — for personal use. Please respect SoundCloud's and Poolsuite's terms of service.

About

Stream Poolsuite tunes to your local Roon server, Bluesound or Sonos

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages