Skip to content
ingoauPublic

Latest commit

 

History

589 Commits

Folders and files

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
  • Save whatever is playing: the bot DMs you a link to it, through song.link for YouTube and SoundCloud so you can find it on your streaming service
  • 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
  • Video mode: watch YouTube videos together on a screen share in the huddle, with captions and chapters; links from other sites yt-dlp supports work too
  • Live streams: lofi radio and other live YouTube or Twitch streams play as they arrive, as audio in music mode and as video in video mode
  • Jellyfin: link your own Jellyfin servers with Quick Connect to search, browse shows and play from them, with resume points, chapters, audio languages and captions
  • 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. On the Channel tab of Settings, channel managers can save the session settings they can see as the defaults every new session in that channel starts with, or clear them, and delete sessions saved to the channel; 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.

Anyone in the huddle can open Your data from the You tab of Settings to see what HuddleFM keeps about them (sessions saved to their account, the songs they added, heard, liked and skipped, and their scrobbles) and delete any of it, or all of it at once. The audit log is never deleted.

The host can switch the session to video mode on the Session tab of Settings, to watch YouTube videos together. The bot shares its screen in the huddle for as long as video mode is on, so it needs one of Slack's two screen share slots free. Search finds YouTube videos, and songs already queued play as their official video when there surely is one, or as audio with the artwork card when there is not. Autoplay, Huddle mix, lyrics, crossfades, ducking and scrobbling pause until the host switches back to music, and their settings are left as they were. The player gains Jump to (a video's chapters, or a time) in place of 💖, and a CC button for captions, which are off by default. In music mode a YouTube video link still plays as audio; the add dialog says so, and offers the host a button to add it and switch, or anyone else a button to add it and ask the host, who gets a direct message to switch or say not now (which stops asking for 15 minutes).

Pasting a live stream adds it as 🔴 LIVE. It plays as it arrives until someone skips it or it ends, so the queue waits behind it and autoplay doesn't kick in. It can't be seeked, resuming it after a pause joins it where it is now, and it is never scrobbled. If it drops, the bot picks it up again twice before skipping it.

To watch from Jellyfin, paste a link to a movie, show or episode from Jellyfin's web app (the address bar on the item's page) into Add to queue. The first time, the bot shows a Quick Connect code and a button that opens your Jellyfin with it filled in; once you approve it, the item is queued, and the dialog says so. You can also link servers, see them, and unlink them under Settings › You. After that the add dialog has a YouTube/Jellyfin source picker, shows open their seasons and episodes, part-watched items ask whether to resume, Play something in video mode suggests what you were watching and what was added recently, and CC picks the audio language and captions. Items stream from your server, converted to fit the video limits when they don't already, and your watch progress is reported back to your account. Only you can use the servers you link; others in the huddle see only the title. The server has to be reachable from the internet, and HuddleFM shows up in its dashboard but takes no remote control; removing the HuddleFM device there stops it working until you link the server again. Jellyfin's "Copy stream URL" links contain your sign-in, so the bot refuses them.

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 AI 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 live sessions and all-time listening stats
CANVAS_SECTIONS all Comma-separated canvas sections to show, in order: summary, active sessions, top artists, top tracks, top videos, top video channels, 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 level songs and videos to -14 LUFS
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

Video mode

Variable Default Purpose
VIDEO_DURATION_LIMIT_SECONDS 3600 Longest video allowed
VIDEO_DOWNLOAD_LIMIT_BYTES 1500000000 Largest video download
VIDEO_MAX_HEIGHT 720 Height videos are downloaded and shared at, from 240 to 1080; the share is 16:9
VIDEO_MAX_FPS 30 Frame rate videos are downloaded and shared at, up to 30
VIDEO_SESSION_LIMIT 2 Huddles that may be in video mode at once; each takes about a CPU core. 0 turns it off
VIDEO_CACHE_LIMIT_BYTES 5000000000 Disk kept for prepared videos under data/cache/video, apart from the song cache; 0 disables and clears it
JELLYFIN_ENABLED true Lets users link their own Jellyfin servers; false turns it off

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
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

Media

HuddleFM joins each Huddle's Chime meeting itself. Each Huddle's media runs in its own Bun process, so a crash ends only that session. The video tile is drawn at 540×540 and 24 fps, with a default and a lyrics layout, 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.

In video mode the bot also joins as its own screen share, the way Chime's content share does, and sends the video and the music there instead of the camera tile; the picture follows the audio's position, and captions are burned in when they are on. If the share cannot start or is lost, the camera tile and the bot's own audio take over, and whoever added the video hears why.

Live streams, YouTube videos in video mode, and Jellyfin items are not downloaded first: the media process reads them as they arrive, through the same public-network proxy as every download, and lines up a live stream's picture with its sound by the timestamps each joined at. A YouTube video starts within seconds however long it is, and downloads into the video cache behind it, so it plays from disk the next time. The bot fetches a streaming YouTube video in pieces, retrying any that fail, and hands it to the media process over loopback: read directly, YouTube sends it at only about twice the playback rate and now and then refuses it, which froze playback.

With LOUDNESS_NORMALIZATION on, songs are re-encoded at -14 LUFS as they download. Videos are not: their loudness is measured instead, from the sound alone for a streamed video, and the bot turns them to the same level as they play (lifting a quiet one by at most 6 dB). A video waits up to 3 seconds for its measurement, and one that takes longer starts as it is and glides to its level when the measurement arrives. Each video's loudness is kept for good, so it is levelled from its first second every time after that, even once its file has left the cache.

Chime media needs outbound UDP 3478 or TLS on port 443 to *.chime.aws.

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?