Repository navigation
Music Bots
A music bot is a real TeamSpeak client this app connects to your server. You can run several per server, each with its own queue, volume and playback state.
Once a bot is in a channel, anyone in that channel can control it by typing in the channel chat. This is how most people use it day to day — no web interface needed.
| Command | Description |
|---|---|
!radio |
List available radio stations |
!radio <id> |
Play a radio station |
!play <url, search terms, or Spotify track link> |
Queue it, or start it if nothing is playing — it never cuts off a running track. Search terms are resolved via YouTube search, Spotify links via their public page |
!play |
Resume paused playback |
!queue <url, search terms, or Spotify track link> (alias !add) |
The same as !play <url>; the command also takes show, remove <n>, <n> and clear
|
!stop |
Stop playback — replies "Nothing was playing." if there wasn't any |
!pause |
Toggle pause and resume |
!skip / !next
|
Next track in the queue |
!prev |
Previous track |
!vol (alias !volume) |
Show the current volume |
!vol <0-100> |
Set the volume |
!np (alias !nowplaying) |
Show the current track |
!stream <url, search terms, or Spotify link> [preset] |
Start a video stream, or queue behind one that is already running; optional trailing 480p, 720p or 1080p
|
!streamqueue |
Show what is streaming and what follows, with who asked for each |
!streamskip |
Move on to the next queued video early |
!stopstream |
Stop the video stream, independently of audio playback |
!viewers |
List the clients currently watching the video stream |
By default anyone in the bot's channel can type any command. The Permissions tab on the Music Bots page restricts that per command, by TeamSpeak server group.
- Assign one or more server groups to a command and only members of those groups may run it; everyone else gets a short refusal naming the group they'd need.
- Admin bypass — designate one or more server groups that skip every restriction, so you don't have to add your admin group to each command individually.
- Aliases that share a handler share one entry, so you can't restrict one and leave its twin open:
!skip/!next,!vol/!volume,!np/!nowplaying,!queue/!add.
The default isn't the same for every command, deliberately:
-
Open to everyone until you say otherwise:
!play,!streamand!np/!nowplaying— the commands a fresh install expects everyone to use. -
Closed until you grant a group: everything else (
!stop,!skip,!vol,!queue,!radio, the stream controls…). A command in this state shows a "No group assigned — unusable" badge and, in chat, tells the caller nobody has been granted access yet. This is the point of the feature: privileged controls shouldn't sit open until an admin remembers to lock them down.
The caller's groups are only looked up (via WebQuery) when a command actually has a restriction, so if you use none of this, the bots pay no extra round-trip.
A music bot only needs the host and the voice port, so it also runs on a connection added without an API key — for instance on a rented server whose host gives out no query access. Two things on this page go through WebQuery and differ there:
-
Command permissions can't be assigned. The bot can't look up a caller's server groups, so the Permissions tab only explains that. The commands that are open to everyone by default (
!play,!stream,!np) keep working for everybody; every other command stays refused, whatever else is configured. - The description template isn't shown. Setting a client's description is something only a query connection can do (TeamSpeak 6 refuses it from the bot's own connection), so the template field carries a note and nothing is sent.
Playing music and the chat commands that are open to everyone work as usual. Adding the API key to the connection later gives the bot those two back without restarting it; removing the key takes them away again.
A bot's card on the Bots tab says what it is doing while it isn't connected (backend 2.53.0 and frontend 3.67.0 or later):
- Connecting — an attempt is running right now.
- Retrying — the last attempt failed. The card shows the retry number (of 10) and a countdown to the next attempt. The pauses grow with every failure, and the Stop button cancels the retries.
- Failed — no further automatic attempt. Either the bot gave up after the 10th retry, or the server refused it for good (a real ban, a wrong server password, a full server). Press Start to try again once the cause is fixed; a manual start counts from one again.
Each state shows the reason below the headline in the server's own words — a ban's reason and the time left, the flood protection, an unresolvable host, a timeout — with a plain sentence for each kind of refusal. A Start that fails shows the same reason in its notification instead of a generic error. The list refreshes by itself every few seconds while a bot is connecting or waiting. Causes and fixes: Troubleshooting.
The Edit dialog of a bot (Bots tab, pencil on its card) has a read-only Unique ID (UID) field with a copy button (backend 2.56.0 and frontend 3.70.0 or later). It shows the ID the TeamSpeak server lists for that bot as its unique ID, so you can recognise the bot on the server side, for example with the search by unique ID on the Client Database page.
- It belongs to the identity the app created for the bot, so it is there while the bot is stopped as well, and it stays the same for as long as the bot exists.
- The dialog for a new bot has no such field, because the ID only exists once the bot has been created: open Edit afterwards.
- The server only has an entry for the bot after its first connection, so anything that needs that entry (a server group, for instance) comes after the first start - see below.
- An empty field with a notice means the bot has no stored identity and connects with a new random one each time, so there is no fixed ID to show. Bots are created with an identity, so this should only affect a damaged or very old record.
A radio bot that plays to an empty channel all day still uses bandwidth and CPU. The Create/Edit dialog has a switch Pause when alone, below Autoplay, off by default (backend 2.60.0 and frontend 3.74.0 or later). Switched on, a field Pause after (minutes) appears, starting at 5. Before frontend 3.86.2 there was only the minutes field, where 0 meant off.
- What it does. Switched on, the bot pauses once it has been playing and no person has been in its channel for that long, and carries on with the same thing as soon as somebody comes in: a radio station is tuned in again (a live stream has no position to keep, so it simply carries on live), a track continues where it stopped.
- Who counts as company. Every voice client in the bot's channel except the bot itself and your other bots on the same server. ServerQuery clients (a query login, this app's own connection) don't count. To the bot, bots of other software and people who are muted or away are ordinary clients, so they do count.
-
Only its own pause is undone. A pause you made yourself (the Pause button,
!pause) stays until you resume it. Starting something else or pressing Stop ends the rule's claim on a pause, and switching the rule off continues a pause it had made. A stop of more than half a minute starts the time over, so pressing Play in an empty channel isn't paused again within seconds. - Autoplay is unchanged. A bot with a radio station as its autoplay starts it even into an empty channel and pauses after the time you set.
- Where it gets the information. The bot learns who is in the channel from its own connection to the server, so this also works on a connection without WebQuery, and a bot that is moved to another channel (by a bot flow, for instance) follows the new channel's occupants.
Pause and Resume also work on a radio station now: Pause stops the stream and Resume tunes it in again. Before, a paused station kept buffering in the background and Resume ended it.
Checked against a TeamSpeak 6.0.0-beta13.1 test server with a real station: the bot paused a minute after the last person left and carried on the moment somebody entered.
The server's anti-flood protection treats a bot like any other client. A bot that sends many commands in a short time is throttled: the server refuses most of them with client is flooding (error 524) instead of carrying them out. Keep that off your bots:
- Create a server group for them (for example "Bots") and give it the permission
b_client_ignore_antiflood(Ignore Antiflood) — see Server and Channel Groups. - Add each bot to that group after its first connection. The server only knows a bot's identity from then on, so this is a step after creating and starting the bot, not before — bots are normally set up one after another, and each one needs it once.
In a test on a TeamSpeak 6 server with the default limits, a bot that sent 400 commands in a burst had 391 of them refused without the permission and none with it.
This covers a bot's commands only. A bot in that group was refused just like any other when the server's per-IP limit on new connections was exceeded — if you see "banned (flood prevention …)" when a bot connects, see Troubleshooting.
Audio and video live in a shared folder on the backend (MUSIC_DIR, see Configuration). You can get files in there three ways:
- Upload through the Library tab
- Download by URL through the Library tab — kept permanently
- Scan for new files, which picks up anything already sitting in the folder, for instance a volume shared with another application
New uploads and downloads are sorted into music/ and video/ subfolders automatically; anything sitting loose in the root is still found.
Neither chat command can interrupt a running track - that is deliberate, so nobody in the channel can cut off what everyone else is listening to. Interrupting on purpose is done from the web interface, where the Play Music dialog offers Play and Queue next to each other for both library tracks and previously requested links.
"Keep played songs" (Settings → YouTube) controls only the ad-hoc downloads made by
!play,!queueand!stream. Turn it off and those are deleted about an hour after playing, to save disk space. Anything you added deliberately through the Library tab is always kept.
Sources can be YouTube, Twitch, a direct URL, or a video already in your library. YouTube and Twitch sources are downloaded once before playback starts rather than streamed live from the CDN — that avoids a rate-limited source and gives noticeably better quality, at the cost of a short wait before the stream begins.
!stream behaves like !play: if nothing is streaming it starts one, and if something already is, the new video is lined up behind it rather than cutting off whatever everyone is currently watching. The reply tells you the position. When a video plays through, the next one takes over without the stream going down — viewers stay connected and keep watching. Only an empty queue stops the stream.
!streamqueue prints what is playing and what follows; !streamskip moves on early. Removing a single entry, and switching over immediately instead of queueing, are done in the web interface: the source field there offers both Queue and Switch now, and the Up Next panel lists the queue with a remove button per entry and a skip button.
A queued link is only fetched when its turn comes, so queueing ten videos does not download ten files up front. The flip side is that a link can have gone stale by then — deleted, region-locked, or longer than the duration limit. Such an entry is skipped with a log line and the queue carries on; it does not end the stream.
Queueing is per stream, not per bot.
!stopstreamis an explicit "that's enough" and discards anything still queued.
Presets are 480p (1500k, 30fps), 720p (4000k, 60fps) and 1080p (6000k, 60fps). A preset sets resolution, frame rate and bitrate together.
What a bare !stream <url> uses is configurable under Settings → Streaming — resolution, frame rate, bitrate and volume, with one button to put the shipped values back. The same tab sets when a stream nobody watches ends, see below. Naming a preset in chat still wins for that one stream. Raise or lower these to match what your server can actually push: both upload bandwidth and CPU are shared by every bot on the machine, and viewers reporting stutter is the signal to come down a step.
Settings → Streaming has a field End the stream when nobody is watching (minutes) (backend 2.58.0 and frontend 3.72.0 or later). A stream with no viewer for that long is ended by the bot, so a forgotten stream doesn't keep using upload bandwidth and CPU. 0, the default, means never; the field takes whole minutes up to 1440.
- When the clock runs. It starts when the last viewer leaves, or when the stream starts if nobody ever joined, and a viewer joining resets it.
- Who counts. The viewers listed in the web interface. The preview player there does not count. While someone is in the list the clock doesn't run. A client that was shut down hard (killed in the task manager, say) leaves the list by itself after about 20 to 30 seconds, as seen on a real server, and the clock starts then; an entry that stays longer can be removed with its remove button.
-
What happens at the end. The bot ends the stream exactly as
!stopstreamdoes, which also discards anything still queued. - Changing the value applies to a stream that is already running and has been empty for a while as well: a value below the time already waited ends it at once, and 0 lets it run on.
Checked against a TeamSpeak 6.0.0-beta13.1 test server with a real bot and sidecar: with 1 minute set and nobody watching, the stream ended 60 seconds after it started, and a viewer joining kept it running past that point.
Streams start at 10% of the video's own level, and that is almost certainly what you want. A video is mastered at whatever loudness its creator chose, which is routinely far above the level people speak at in a channel — a stream at its own level arrives as a shout. Viewers can turn their own player up; they cannot turn down something that is already too loud for them.
The percentage is set under Settings → Streaming and applies from the next stream on. There is no control on a running stream on purpose: ffmpeg fixes the level when it starts, so changing it mid-stream would mean restarting the video for everyone watching. Decide before you start, or restart the stream.
The level in force is shown in the stream's status line in the web interface, next to the preset — worth a look if someone reports that a stream is inaudible.
The web interface has a preview player with a mute toggle, and a live viewer list where you can remove individual viewers.
Video needs the sidecar container running, and its UDP ports reachable — see Deployment, which covers the single most common reason streaming fails.
From the Create/Edit dialog you can set a custom avatar and a description template that updates live.
Description placeholders:
| Placeholder | Meaning |
|---|---|
{title} |
Title of the current track |
{artist} |
Artist, if known |
{remaining} |
Time left in the current track, formatted ("42 min", "1h 5min"); empty for live streams |
{remaining_min} |
Time left across the whole queue — this track plus everything after it — as plain minutes; empty while the current track's length is unknown |
{elapsed} |
Time played so far |
{duration} |
Total track length; empty for live streams |
{queue_length} |
Number of tracks still queued after this one |
The description refreshes whenever something actually changes, plus roughly every 30 seconds while playing — but only if your template contains a time placeholder, so it isn't polled needlessly otherwise. When nothing is playing it shows an idle form rather than going blank.
⚠️ The avatar is currently broken by a TeamSpeak 6 server bug, not by this app: it displays correctly to TS6 clients but hangs on "Loading Image" in TS3. This was isolated to the server software and reported upstream. The description template is unaffected.
Music Request History (System in the navigation, admins only) lists the songs people have asked the bots for, newest first: the title, how long ago it was requested, and a link that opens the source (the YouTube page, say) in a new tab.
- Play it again: pick one of the server's connected bots in the selector at the top, then use Play (interrupts what is running) or Queue (lines it up behind it, or starts it if the bot is idle) on an entry. With no bot of the server connected, the buttons stay disabled.
- The same list in the play dialog: a bot's Play… dialog has a History tab with these entries and the same two buttons, for that bot.
- Loading takes a moment: a song that is not downloaded yet is fetched first, which can take several seconds. The clicked button shows a spinner until the song starts.
-
What is recorded: a song that is fetched from a link or a search — through
!playand!queuein the chat, or the same actions in the web interface. Radio stations, tracks that are already in the library and video streams (!stream) are not. - One row per link. Asking for the same song again does not add a second row; it moves the existing one to the top with the new time.
- Per server connection, newest 25. The list belongs to the selected connection (all its virtual servers and bots together). Only the latest 25 entries are kept; older ones are deleted when a new song is requested. It records the song, not who asked.
- There is nothing to delete or clear by hand, and it does not need WebQuery, so it is also there on a connection added without an API key.
yt-dlp needs cookies for those. Manage them under Settings → YouTube — upload a cookies.txt or paste its contents. They are validated against YouTube on upload, every six hours, and on demand, because cookies rotate and quietly go stale; a banner appears when they stop working.
v0.18, last edited 22.10.2026
Getting started
Using the app
Running it
When something is odd
Contributing