Skip to content

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

  • Long playlists: when a YouTube, SoundCloud, Bandcamp, Spotify, Deezer or Apple Music list has more than MAX_PLAYLIST_SIZE (30) tracks, /play asks with buttons whether to load the first 30 or the whole playlist (only the requester can answer; after 30 s the first 30 load). A radio or mix link asks once, with a third “This track only” button. The clip page shows the same choice in a small dialog. The whole playlist is capped by the new MAX_PLAYLIST_FULL setting (200) and by the room left in the queue (/limits).
  • Spotify without API credentials says when its embed page may have hidden tracks beyond the first 100.
  • New optional settings in .env (see .env.example): VOTESKIP_PERCENT, MAX_QUEUE_SIZE, MAX_TRACKS_PER_USER, MAX_TRACK_MINUTES, PLAY_COOLDOWN_SECONDS, STAY_247, YTDLP_COOKIES, BACKUP_INTERVAL_HOURS, BACKUP_KEEP, LASTFM_API_KEY / LASTFM_API_SECRET, LIVE_MAX_HOURS and SHARD_COUNT. Every default keeps the 1.4 behaviour, except the queue, which is now capped at 200 tracks (MAX_QUEUE_SIZE).
  • New files in DATA_DIR: voteskip.json, limits.json, stay.json, lastfm.json (0600, holds Last.fm session keys) and backups/. Backups contain those secrets: store them like a password.
  • YouTube cookies: mount a cookies.txt read-only outside ./data (see docker-compose.yml) and set YTDLP_COOKIES; use a throwaway Google account.
  • Backups: docker compose exec dj-snake python -m djsnake.backup create; restoring needs the bot stopped (see the backup docs).
  • Saved playlists can no longer be named queue (it means the live queue in /playlist export).
  • New slash commands (/remove, /move, /previous, /clear, /nowplaying, /history, /voteskip, /limits, /stay, /filter, /lastfm) take up to an hour to appear when synced globally.
  • Queue commands: !remove <position> and !move <from> <to> (positions as !list shows them, with a queue autocomplete on the slash versions), !previous and !clear. !move, !previous and !clear need the DJ role when one is set; !remove also works on a track you queued yourself.
  • !nowplaying (!np): an embed with the title, requester, source, artwork, a progress bar, the loop, autoplay and volume state and the clip page link (the view-only one when control is limited).
  • !history [count]: the last tracks played in the session (default 10, max 25), newest first; it survives !reset.
  • Vote to skip: /voteskip (!vs) for members in the voice channel who cannot use /next. More than VOTESKIP_PERCENT (50) of the non-deafened humans in the channel must vote; DJs and the requester skip at once, votes are recounted as people join, leave or deafen and reset with each track. /voteskip threshold <1-100> (DJs) sets it per server. The panel and the clip page show the count.
  • Queue limits against abuse: queue size, tracks per member, track length and a /play cooldown, with .env defaults and /limits (Manage Server) per server. They apply to /play, the page’s add, playlist loads and autoplay (queue size only); a too-long single link is refused from its metadata, before anything is downloaded. DJs skip the per-member limit and the cooldown.
  • 24/7 mode: !stay [on|off] keeps the bot in its voice channel (no idle disconnect, !stop does not leave) and rejoins it after a restart. Dropped by Discord, it rejoins once, then turns itself off; !reset and channel moves do not count as drops.
  • Saved playlist import and export: /playlist import <name> <link> from a YouTube, SoundCloud, Bandcamp, Spotify, Deezer or Apple Music list or album, or from an attached JSON, M3U or TXT file; /playlist export <name> [json|m3u|txt] (or queue). One import runs at a time per server.
  • Audio filters per session: !filter [off|bassboost|nightcore|vaporwave|8d|karaoke|soft] and !filter eq <low> <mid> <high>, also on the clip page. A change restarts the track where it is; nightcore and vaporwave change the real speed and the page follows it.
  • Live streams: YouTube lives, Twitch channels and internet radios (Icecast, Shoutcast, .pls, .m3u) play straight from the stream, with a LIVE badge and a radio’s current song on the clip page. Seek and crossfade are off for them, LIVE_MAX_HOURS (4, pauses not counted) stops a forgotten one, and they are left out of the listening stats.
  • Optional YouTube cookies (YTDLP_COOKIES) for age-restricted and members-only videos, with specific messages for age-restricted, members-only, private and sign-in-required videos.
  • Optional Last.fm scrobbling (LASTFM_API_KEY, LASTFM_API_SECRET): /lastfm link|unlink|status; linked listeners who are not deafened get “now playing” and scrobbles.
  • Backup and restore of DATA_DIR: python -m djsnake.backup create|restore|verify|list (checksummed .tar.gz, SQLite copied consistently while the bot runs), and automatic rotated backups with BACKUP_INTERVAL_HOURS and BACKUP_KEEP.
  • Spanish and German (!language es|de): bot replies, !help, the status message and the clip page. scripts/i18n-missing.py lists missing translations.
  • Clip page: Media Session support (lock screen, headset and media keys; control only with the control link).
  • Clip page: installable as an app (PWA), following the server’s theme name, colour and logo, with a “Session offline” page; the API, streams and media are never cached.
  • Clip page: keyboard shortcuts and a help dialog (?).
  • Sharding: the bot is an AutoShardedBot with an optional SHARD_COUNT; /healthz and /metrics report each shard.
  • End-to-end tests (pytest -m e2e, a new CI job): the real FFmpeg playback chain and the clip page in headless Chromium, plus a manual checklist for a real Discord server.
  • /healthz reports status and discord as degraded (still 200) when only some shards are down; a shard briefly closed by a gateway resume still counts as up. Prometheus latency gauges print +Inf instead of NaN without a connection.
  • Every module that saves JSON in DATA_DIR uses one atomic write helper (fileio.py).
  • Clip page: at 320 px wide (and Cobalt at 360 px) the loop button of the transport row was clipped. The row now tightens its buttons and spacing on narrow phones.
  • A flaky control-link rotation test.
  • Internet radios cannot reach the bot’s own network: the bot resolves a radio’s host once, requires a public address and connects to that very address (no DNS rebinding), re-checks every redirect, reads the stream itself and pipes it to FFmpeg, which may open nothing else (-protocol_whitelist pipe). HLS radios are refused. YouTube and Twitch lives are read by FFmpeg with HTTP(S) and HLS protocols only, and their expiring URLs are refreshed just before playing.
  • The private copy of YTDLP_COOKIES is deleted on failure and at exit; cookies.txt files are ignored by git and Docker.
  • backup restore holds the data lock for the whole restore, refuses archive members whose size differs from the manifest and checks free space first.
  • Playlist import refuses deeply nested JSON instead of crashing.
  • GET /healthz: 200 with status, Discord latency, guild and player counts, uptime and version when the bot is connected, 503 with the reason otherwise.
  • Docker HEALTHCHECK on /healthz (60 s start period), so docker compose ps shows whether the bot is healthy.
  • Optional GET /metrics in the Prometheus text format (players, queued tracks, downloads, viewers, Discord latency, tracks played), off unless METRICS=1.
  • Search by text: !play daft punk one more time (any argument that is not a link) and the clip page’s add box search YouTube and queue the best match, scored like Spotify tracks. When nothing scores high enough the bot says so.
  • Loop and shuffle. !loop [off|track|queue] (no argument cycles) replays the current track or sends finished tracks back to the end of the queue; !shuffle shuffles the upcoming queue once. The clip page has matching shuffle and loop buttons, the control API gets loop and shuffle actions, /api/{sid}/state exposes loop, and the status panel shows the loop mode.
  • Per-server volume: !volume shows it, !volume <0-150> sets it (percent, 100 = the normalised level). It applies to the playing track without a restart, glides over a few frames to avoid clicks, is soft-limited above 100%, works with crossfade and is saved in DATA_DIR/volume.json.
  • Clip page: a volume button with a slider popover next to Sync, a volume control action and a volume field in /api/{sid}/state. The status panel shows the volume only when it is not 100%.
  • Autoplay (“radio”): !autoplay [on|off], a switch on the clip page and the AUTOPLAY default. When the queue runs out, a track related to the last one (YouTube’s RD<id> mix) is prepared during playback and plays next; recent tracks, live streams, tracks over 10 minutes and non-music titles are skipped, and the bot lingers idle when nothing is found. Loop modes take precedence. Autoplay tracks are marked in the status panel and on the page.
  • Saved playlists per server: !playlist save|add|load|show|delete <name> and !playlist list, kept in playlists.db under DATA_DIR (links and titles only, up to 50 playlists of 200 tracks). The clip page can save the queue and load a playlist (GET /api/{sid}/playlists, save_playlist and load_playlist control actions).
  • SoundCloud and Bandcamp links: !play and the clip page’s add action accept tracks and sets/albums (soundcloud.com, *.bandcamp.com). Playlists respect MAX_PLAYLIST_SIZE; the page shows the artwork for tracks without video (now_playing.audio_only in the state), and the “Downloading” list names the platform.
  • Deezer and Apple Music links (tracks, albums, playlists), read without credentials (Deezer’s public API, Apple’s iTunes lookup API and playlist page) and matched on YouTube like Spotify tracks (new djsnake/catalog.py).
  • Synced lyrics on the clip page: a Lyrics button shows the words over the clip, the sung line highlighted and scrolled in time, or plain lyrics when no timing exists. Lyrics come from LRCLIB (free, no key), are looked up in the background when a track is queued and cached in DATA_DIR/lyrics/. LYRICS=0 turns them off.
  • Slash commands: every command also works as a slash command (/play, /next, /list, /seek, /pause, /resume, /stop, /reset, /crossfade, /theme, /loop, /shuffle, /volume, /autoplay, /playlist, /djrole, /weblink, /language), with suggestions for the preset and playlist names. The ! versions are unchanged. Re-invite the bot with the applications.commands scope if the commands do not show up. SLASH_SYNC_GUILD publishes them to one server at once, for development, and the number of synced commands is logged at startup.
  • Live updates: the clip page gets each change pushed by the bot over a Server-Sent Events stream (GET /api/{sid}/events) instead of polling every 2 seconds, and falls back to polling when the stream cannot get through. Behind nginx, turn off proxy_buffering (see “Exposing the page”).
  • View-only clip page link: every session also has a second unguessable link that shows the same page with the controls hidden and a “View only” badge; the server refuses controls sent with it (403).
  • DJ role: !djrole <role>|off (Manage Server, saved in DATA_DIR/permissions.json). When set, !next, !pause, !resume, !seek, !crossfade, !loop, !shuffle, !volume, !autoplay, !stop and !reset need the role or Manage Server (whoever queued the playing track may still skip it), and the status message shows only the view-only link. Changing the role gives a live session a new control link and retires the server’s !theme edit links.
  • !weblink DMs the control link and the view-only link (DJs only when control is limited).
  • WEB_CONTROL=everyone|dj (default everyone): with dj, the control link is never posted publicly.
  • French translation, and a framework for more languages: !language en|fr (Manage Server) switches a server’s command replies, errors, !help, status message and web API error messages; the clip page follows it (or the browser’s language) and has a language picker in its settings menu. BOT_LANGUAGE sets the default (en). Messages live in djsnake/locales/{en,fr}.json.
  • The “link not accepted” message lists every supported source.
  • !play now rebuilds links like the clip page’s add action (same host allowlist, canonical URL); links from other hosts are refused with the “link not accepted” message.
  • The status message now shows the view-only link next to the control link. With the defaults, everyone can still control the music as before.
  • Session ids are no longer written to the logs.
  • Theme preset cobalt: a flat ultramarine page with blush-pink and butter-yellow accents, wide Unbounded headings, borderless rounded panels and springy controls (!theme cobalt, WEB_THEME, base for custom).

Version 1.2.0 was never published as a Docker image or GitHub Release (its release build failed); use 1.2.1 instead.

  • The arm64 Docker image failed to build on Python 3.15: davey has no prebuilt aarch64 wheel there, so it is now compiled from source in a separate builder stage, keeping the compiler out of the final image.

Upgrading: the bgutil PO token plugin and the pot-provider image were bumped together to 2.0.2, so pull the new image too (docker compose pull && docker compose up -d).

  • Listening stats: a chart button in the clip page header opens the server’s stats for the last week, month or all time: top tracks, artists, most skipped track, hours played, tracks, sessions, peak listeners and a day-by-hour heatmap in the viewer’s timezone. Plays are recorded in DATA_DIR/stats.db (SQLite) without ever blocking playback. New route GET /api/{sid}/stats?range=week|month|all.
  • Live viewer count (”👁 N”) in the clip page header; the state JSON gains viewers.
  • “Downloading” section on the clip page, under “Up next”, listing every track asked for but not queued yet (links, playlist and Spotify entries). The state JSON gains downloading and downloading_total, and the status panel shows the count.
  • Discreet “Now playing” row above “Up next” on the clip page.
  • Idle linger: when the queue runs out the bot stays in voice for IDLE_DISCONNECT_SECONDS (default 300) before leaving, so a track added meanwhile plays at once. It leaves after 30 s if nobody is in the channel, and !stop still leaves at once.
  • Theme preset space (!theme space, WEB_THEME default, base for custom).
  • User documentation on Read the Docs (https://dj-snake.readthedocs.io), built with Sphinx, MyST and Furo; CI builds it with warnings as errors.
  • The live demo opens on a simulated Discord channel driving the real clip page.
  • Clip page redesign shared by every theme: video, title, seek bar and controls form one player block, with a soft glow from the artwork; the Minimal theme uses Geist.
  • The web page and errors are now in English. The nuit, vinyle and perso themes are now night, vinyl and custom; saved themes are migrated automatically.
  • Docker image now runs on Python 3.15, CI tests Python 3.10 to 3.15, and the yt-dlp floor is 2026.10.21.
  • Dependencies bumped: discord.py 2.7.1, python-dotenv 1.2.4, aiohttp 3.14.5, PyNaCl 1.6.2, davey 0.1.7, audioop-lts 0.2.2, bgutil-ytdlp-pot-provider and pot-provider image 2.0.2, dev tools and GitHub Actions.
  • The clip page could stay stuck on a stale track: polls now time out and a watchdog makes the page always converge to the playing track.
  • Stats: listener peak recorded after the write, flush at shutdown, short-track skip rule.
  • “DJ of the week” and top DJs from the stats panel (the requester is still recorded but never shown).
  • Spotify links: !play and the clip page’s add action accept Spotify playlists, albums and tracks (open.spotify.com links and spotify: URIs). The track list is read from the public embed page (no configuration, 100 tracks max) or from the Web API when SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET are set. Each track is searched on YouTube and the best match (duration, title and artist, official video first) is queued; tracks without a match are skipped and counted. Nothing is downloaded from Spotify.
  • GitHub link in the clip page header.
  • Long track titles are clamped to two lines, and queue titles are more readable.
  • Refreshed demo and showcase (README, project page, social preview), which now highlight Spotify links.
  • Discord music bot: !play downloads audio and video from YouTube with yt-dlp and plays it in a voice channel, with an independent queue per server.
  • Playlists: whole playlists load in the background (up to MAX_PLAYLIST_SIZE), and radio/mix links ask whether to play one track or the full list.
  • Playback commands: !list, !next, !pause, !resume, !seek, !stop and !reset.
  • Clip live page: every voice session gets a link to a synced web page that shows the muted video of the current track, the queue and a progress bar.
  • Web controls: skip, previous, pause, resume, seek, jump to a queued track, reorder, remove and add YouTube links from the page.
  • Automix: off-topic segments are cut with SponsorBlock, loudness is normalised, silence at the start and end is trimmed, and tracks fade in and out.
  • Optional crossfade between tracks (!crossfade on|off or from the page).
  • Live status message per session, edited in place instead of spamming the channel.
  • Selectable clip page themes, with a per-server default and a theme editor.
  • Docker Compose deployment with a PO token provider sidecar to get past YouTube’s “Sign in to confirm you’re not a bot” check.
  • yt-dlp self-update on every container start, plus a weekly refresh script for long-running hosts.
  • Retries with backoff for transient download failures.
  • Sessions expire after SESSION_IDLE_HOURS without activity and clean up their files.
  • Automated test suite (pytest) run in CI, with the code split into the djsnake/ package.
  • Prebuilt multi-architecture (amd64, arm64) Docker images on GitHub Container Registry.
  • HTTP 403 errors on YouTube downloads.
  • Queued tracks being wiped on routine gateway reconnects.
  • Python 3.13+ compatibility.