Skip to content

Repository files navigation

Warning: This codebase is almost entirely generated by AI, and most of the code hasn't been read. Use at your own risk.

HuddleFM

HuddleFM is a self hosted music bot for Slack huddles. Invite it to a Huddle and participants can search for songs, albums, playlists from YouTube, or add supported media links; build and reorder a shared queue; and control playback, seeking, volume, autoplay, loop, and permissions directly through Slack.

Features

  • Search for songs, albums, and playlists from YouTube
  • Add supported media links (e.g. YouTube, SoundCloud, Navidrome shares) to the queue
  • Build and reorder a shared queue, or shuffle the pending queue in one click
  • Control playback, volume and settings, directly through Slack
  • Set permissions for who can control playback and queue
  • View album art, progress, and read lyrics streamed as a camera feed
  • Automatically scrobble to Last.fm and Listenbrainz
  • Autoplay: Related (YouTube up-next) or Huddle mix (Last.fm, ListenBrainz, and songs people have added)
  • Play something: one click on an idle player starts Huddle mix, falling back to a currently popular song when there is no listening history yet
  • 💖 whatever is playing to hear more like it: a one way nudge to your own recommendations that nobody else in the huddle sees, with an undo
  • Loop: repeat the current track or cycle the finished queue
  • Auto-duck: lower the music while someone is speaking in the huddle, and bring it back when they stop
  • Support for simultaneous huddles
  • End of session recaps & a statistics canvas
  • Auto leave when inactive
  • Self hosting with a single Docker container

Usage

Invite the bot to a huddle or ping it in the thread. It will join the huddle and send a UI in the thread to add songs to the queue and control playback. You'll be the host by default. Managers of the huddle's channel and workspace admins can end the session from Settings, even when they aren't in the huddle; whoever runs the bot can change how much they may do with CHANNEL_MANAGER_PERMISSIONS and WORKSPACE_ADMIN_PERMISSIONS. If the UI gets buried by conversation in the thread, mention the bot with nothing else to bring it back to the bottom, or keep it at the bottom by enabling anchor in the settings.

With OPENROUTER_API_KEY set, you can also @mention the bot with a request (for example @HuddleFM add something by Radiohead, @HuddleFM skip, or @HuddleFM turn autoplay on). It uses a cheap Gemini model through OpenRouter and the same permissions you already have in the player UI. Replies are ephemeral. This also works in the original huddle thread when the session uses a forced companion controls channel (where the bot cannot post publicly).

Self hosting

HuddleFM ships as a single Docker image (ghcr.io/ingoau/huddlefm:latest). You need a dedicated Slack user for the bot, plus both a Slack app (Socket Mode) and browser session credentials from that same user so it can join Huddles.

Quickstart

  1. Put compose.yaml in a folder:
curl -fsSL -o compose.yaml https://raw.githubusercontent.com/ingoau/huddlefm/main/compose.yaml
  1. Create .env next to it with the required credentials:
SLACK_WORKSPACE_URL=https://example.slack.com
SLACK_XOXP=xoxp-...   # Slack app user token (Web API)
SLACK_XAPP=xapp-...   # Slack app-level token (Socket Mode)
SLACK_XOXC=xoxc-...   # Browser session token for joining Huddles
SLACK_XOXD=xoxd-...   # Browser `d` cookie for that session

SLACK_XOXP and SLACK_XOXC/SLACK_XOXD must belong to the same Slack user. HuddleFM checks this on startup.

On Enterprise Grid the client token can be set as SLACK_ENTERPRISE_XOXC instead of SLACK_XOXC; it takes precedence when both are set. Either way one of the two is required.

  1. Start it:
docker compose up -d
  1. Invite the bot to a Huddle (or mention it in the Huddle thread). Session state and logs live under ./data.

That is enough for a working deploy. Optional features (scrobbling, analytics, AI @mentions, timeouts, and more) are listed under Configuration. .env.example has the same options as commented defaults.

Configuration

Required

Variable Purpose
SLACK_WORKSPACE_URL Workspace URL, e.g. https://example.slack.com
SLACK_XOXP User OAuth token used for Slack Web API calls
SLACK_XAPP App-level token used for Socket Mode
SLACK_XOXC Client token from the bot user's browser session
SLACK_ENTERPRISE_XOXC Enterprise Grid client token; used instead of SLACK_XOXC when set
SLACK_XOXD d cookie from the same browser session

Optional Slack behavior

Variable Default Purpose
MANAGER_USER_ID unset User who is always treated as a host
CHANNEL_MANAGER_PERMISSIONS end What managers of a huddle's channel may do without joining: none, end the session, or everything a host can
WORKSPACE_ADMIN_PERMISSIONS end The same for Slack workspace admins and owners. Replaces WORKSPACE_ADMINS_AS_MANAGERS=true, which still means host until it is set
EXCLUDED_USER_IDS unset Comma/space-separated users ignored for participation, hosting, permissions, and scrobbling
FORCE_COMPANION_CHANNEL_IDS unset Channels that always get a separate HuddleFM controls channel
SLACK_TEAM_ID unset Workspace for companion channel creation; required for Enterprise Grid credentials
SLACK_CANVAS_ID unset Canvas updated with all-time listening stats
CANVAS_SECTIONS all Comma-separated canvas sections to show, in order: summary, top artists, top tracks, top channels, controls, integrations
FOOTER unset Optional mrkdwn footer under the queue controls

Optional features

Variable Default Purpose
LASTFM_API_KEY / LASTFM_SHARED_SECRET unset Enables Last.fm account linking for scrobbling
OPENROUTER_API_KEY unset Enables @mention AI controls via OpenRouter
POSTHOG_API_KEY unset Enables PostHog analytics and error tracking
POSTHOG_HOST https://us.i.posthog.com PostHog ingestion host
LOCAL_CONTROL_TOKEN unset Bearer token for local /join, /leave, and /tone routes; leave unset for typical Docker deploys

Playback and limits

Variable Default Purpose
QUEUE_LIMIT 50 Maximum tracks in the queue
TRACK_DURATION_LIMIT_SECONDS 1200 Maximum track duration
TRACK_DOWNLOAD_LIMIT_BYTES 100000000 Maximum download size
INITIAL_VOLUME 0.5 Starting volume as a fraction of max
DUCKING_MODE gentle Auto-duck default: off, gentle, strong
LYRICS_OFFSET_MS 0 Shift the lyrics' timing; positive shows them later, for when they run ahead of the audio
LOUDNESS_NORMALIZATION false Set to true to enable -14 LUFS normalization
TRACK_PREPARATION_CONCURRENCY 2 Tracks downloaded and analysed at once, across all Huddles
MEDIA_CACHE_LIMIT_BYTES 1000000000 Disk kept for prepared tracks under data/cache/media; 0 disables and clears the cache
MEDIA_CACHE_MAX_AGE_DAYS 30 Re-download cached tracks older than this; 0 keeps them until evicted for space
ALONE_TIMEOUT_MS 120000 Leave when alone for this long
IDLE_TIMEOUT_MS 600000 Leave when idle for this long
PAUSED_TIMEOUT_MS 600000 Leave when paused for this long
CHIME_MEDIA_REGION ap-southeast-2 AWS region for Chime media

Runtime and logging

Variable Default Purpose
BIND_ADDRESS 127.0.0.1 HTTP bind address (loopback is fine; Compose does not publish ports)
PORT 3210 HTTP port
MEDIA_BACKEND browser native or native-with-fallback (experimental, see below)
CHROME_PATH /usr/bin/chromium in the image Chromium executable path
LOG_LEVEL info Minimum operational log level (debug, trace, …)
LOG_FILE data/logs/huddlefm.jsonl Rotated JSON log path; set empty to disable file logging
LOG_FILE_SIZE 10m Maximum size of each log file
LOG_FILE_COUNT 7 Rotated files retained in addition to the active file

Native media backend (experimental)

With MEDIA_BACKEND=native, HuddleFM joins each Huddle's Chime meeting itself instead of through headless Chromium. Each Huddle's media runs in its own Bun process, which uses much less memory and CPU and joins faster.

Everything else behaves the same, except that the video tile is drawn natively at 540×540 and 24 fps. It has the same default and lyrics layouts as the browser's, with syllable-synced lyrics, a moving backdrop while a track plays, and songwriter credits after the last line. Lyrics with no timing fall back to the default layout.

Chime media needs outbound UDP 3478 or TLS on port 443 to *.chime.aws, the same as the browser backend. Leave the variable unset to keep the browser backend.

With MEDIA_BACKEND=native-with-fallback, Huddles start on the native backend but can move to Chromium for the rest of the session. That happens automatically when native media crashes, fails to join, or loses its connection, and when someone presses Playback not working? under the player. The button switches straight away, so the music drops out for a few seconds, then asks what went wrong. A session that fell back stays on Chromium if it is restored.

Each switch gets a JSON file in data/reports, named by time and reportId. It holds why the switch happened, what was playing, the native backend's last 200 log lines and media events at every level (more than the log file keeps at LOG_LEVEL=info), this session's recent app log lines, whether the browser took over, and the form answers once they are sent. The newest 200 files are kept, for up to 30 days. They include Slack user IDs and whatever people type into the form, and stay on the host.

The audit log (data/audit.jsonl) records each switch as media.fallback, with trigger set to automatic or report, and each submitted form as media.problem_reported, both with the reportId. These go to PostHog too when it is configured, which is the easiest way to count how often Huddles fall back.

Updating

docker compose pull
docker compose up -d

Active sessions should restore after a short restart.

Logs

HuddleFM writes structured JSON logs to stdout and, by default, rotated files under data/logs. Those files survive container recreation because data is bind-mounted from the host. Use LOG_LEVEL=debug for detailed API and queue activity, or trace for playback-position events.

Logs can include Slack user, channel, session, and track identifiers. Credentials and known token fields are redacted. Rotation keeps the active file plus LOG_FILE_COUNT older files.

Analytics

Set POSTHOG_API_KEY to send product events and sanitized errors to PostHog. Events use Slack user IDs as distinct IDs and never include Slack names, messages, search text, lyrics, audio, credentials, or raw API payloads. Active users get current-state person properties for their Last.fm and ListenBrainz connections, enabled settings, and scrobbling mode. HuddleFM keeps a random system identity in data/posthog-installation-id. Analytics is best-effort and disabled when the key is absent.

About this project

I Codex built this for the Hack Club Slack because we didn't really have any music bots. We did have a bot that could join huddles, but it was more for video, and required one of the two precious screen sharing slots to be used.

I decided to build a bot that was really intuitive for music, and would just work. I started planning it, going back and forth with Codex and making sure it was on the right track. I was happy with the plan and told it to implement it. One hour later I had something that mostly worked!

I worked on it for a few days, fixing bugs, adding lots of features, and I'm mostly happy with the state of it now! I even got Claude to review it and it said it wasn't just classic AI slop so I guess that's good?